You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Azure AD的ASP.NET实体Author与Editor字段追踪问题

在Azure AD环境下为ASP.NET实体追踪Created By/Modified By字段

我来给你梳理下这个场景下的解决方案,毕竟没有ApplicationUser实体可用,得换个思路从Azure AD的身份信息入手,同时用EF Core的拦截器来自动处理字段填充,不用重复写代码。

第一步:定义可审计的实体基类

先给所有需要追踪的实体建个基类,把通用的审计字段都放进去,这样每个实体直接继承就行:

public abstract class AuditableEntity
{
    // 可以根据需求选存用户ObjectId(更稳定)或者用户名
    public string CreatedBy { get; set; }
    public DateTime CreatedDate { get; set; }
    public string ModifiedBy { get; set; }
    public DateTime ModifiedDate { get; set; }
}

比如你的Product实体就可以这样写:

public class Product : AuditableEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    // 其他业务字段...
}

第二步:获取当前Azure AD用户的信息

因为用Azure AD认证,当前用户的身份信息存在HttpContext的Claims里,我们可以封装一个服务来统一获取,避免到处写重复的Claims查询逻辑:

public interface ICurrentUserService
{
    // 获取Azure AD用户的唯一标识(ObjectId)
    string GetCurrentUserObjectId();
    // 获取用户的显示名称或邮箱
    string GetCurrentUserDisplayName();
}

public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CurrentUserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetCurrentUserObjectId()
    {
        return _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
    }

    public string GetCurrentUserDisplayName()
    {
        // 优先取PreferredUsername(通常是邮箱),没有的话取Identity.Name
        return _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.PreferredUsername) 
               ?? _httpContextAccessor.HttpContext?.User?.Identity?.Name
               ?? "System"; // 后台任务等无上下文场景用"System"标识
    }
}

然后在Program.cs里注册这个服务:

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();

第三步:用EF Core拦截器自动填充审计字段

这一步是核心,通过EF Core的SaveChangesInterceptor,在保存实体前自动处理CreatedBy/ModifiedBy等字段,不用在每个业务逻辑里手动设置:

public class AuditableEntitySaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly ICurrentUserService _currentUserService;
    private readonly TimeProvider _timeProvider;

    public AuditableEntitySaveChangesInterceptor(ICurrentUserService currentUserService, TimeProvider timeProvider)
    {
        _currentUserService = currentUserService;
        _timeProvider = timeProvider;
    }

    // 同步保存时的处理
    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        UpdateAuditableEntities(eventData.Context);
        return base.SavingChanges(eventData, result);
    }

    // 异步保存时的处理
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
    {
        UpdateAuditableEntities(eventData.Context);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void UpdateAuditableEntities(DbContext? context)
    {
        if (context == null) return;

        var currentUser = _currentUserService.GetCurrentUserDisplayName(); // 或者用GetCurrentUserObjectId()
        var currentUtcTime = _timeProvider.GetUtcNow().DateTime;

        // 遍历所有继承AuditableEntity的实体
        foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    // 新增时同时设置创建和修改字段
                    entry.Entity.CreatedBy = currentUser;
                    entry.Entity.CreatedDate = currentUtcTime;
                    entry.Entity.ModifiedBy = currentUser;
                    entry.Entity.ModifiedDate = currentUtcTime;
                    break;

                case EntityState.Modified:
                    // 修改时只更新修改字段,禁止修改创建字段
                    entry.Entity.ModifiedBy = currentUser;
                    entry.Entity.ModifiedDate = currentUtcTime;
                    entry.Property(x => x.CreatedBy).IsModified = false;
                    entry.Property(x => x.CreatedDate).IsModified = false;
                    break;
            }
        }
    }
}

然后把这个拦截器注册到你的DbContext:

builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
           .AddInterceptors(sp => sp.GetRequiredService<AuditableEntitySaveChangesInterceptor>()));

builder.Services.AddScoped<AuditableEntitySaveChangesInterceptor>();

一些额外的注意事项

  • 如果你需要存储用户的唯一标识(比如后续要关联Azure AD用户信息),推荐用GetCurrentUserObjectId()返回的ObjectId,比用户名更稳定
  • 后台任务或者没有HttpContext的场景,CurrentUserService会返回"System",你可以根据需求调整这个默认值
  • 要确保所有需要追踪的实体都继承AuditableEntity基类,否则拦截器不会处理

内容的提问来源于stack exchange,提问作者S. Walker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:28:15