Audit.NET Entity Framework Core - 相关实体管理

m.p*_*bos 5 c# entity-framework-core audit.net

我正在开发 ASP.NET Core 3.1 Web 应用程序。我想在数据库上保存数据时添加审计跟踪/日志。

我从这个 SO answer 中获得灵感,开始在测试项目中使用 Audit.NET。
这是我的目标(类似于相关的 SO 线程):

  1. 将审计记录存储在不同的数据库中:几乎使用额外的AppAuditDbContext;
  2. 每个类型都有一个与审计类型匹配的审计表(带有额外的审计字段):通过对额外的反射完成AppAuditDbContext
  3. 不需要维护单独的审计实体。操作数据库和审计数据库之间的变化应该是无缝的:通过对额外实体和被审计实体的反思来完成AppAuditDbContextDataAnnotations
  4. 检索相关实体的审计数据:TO DO

此时,我可以 CRUD 一个独立的审计实体,并在审计数据库上检索正确的审计。
但是,尽管我可以成功删除父实体及其子实体并获取父实体和子实体的审计数据,但我无法弄清楚如何从数据库中获取分组审计数据以用于此类场景。
我尝试使用 Audit.NET EntityFramework 的EntityFrameworkEvent.TransactionIdEntityFrameworkEvent.AmbientTransactionId,但它们都null在数据库上。

这是我的 POCO

public interface IAuditableEntity
{
    [NotMapped]
    string AuditAction { get; set; }

    [NotMapped]
    string AuditTransactionId { get; set; }

    [NotMapped]
    string AuditAmbientTransactionId { get; set; }
}

public class Scope : IAuditableEntity
{
    [Key]
    public int Id {get;set;}

    public string Name { get; set; }

    public virtual ICollection<Job> Jobs { get; set; }

    [NotMapped]
    string AuditAction { get; set; }

    [NotMapped]
    string AuditTransactionId { get; set; }

    [NotMapped]
    string AuditAmbientTransactionId { get; set; }
}

public class Job : IAuditableEntity
{
    [Key]
    public int Id {get;set;}

    public int ScopeId { get; set; }
    public virtual Scope Scope { get; set; }

    [StringLength(128)]
    public string Name { get; set; }

    [NotMapped]
    public string AuditAction { get; set; }

    [NotMapped]
    public string AuditTransactionId { get; set; }

    [NotMapped]
    public string AuditAmbientTransactionId { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这是我的 Audit.NET 配置(来自 Startup.cs)

public class Startup
    {            
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddDbContext<AppAuditDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("AuditConnection")));

            #region Audit.NET

            var auditDbContextOptions = new DbContextOptionsBuilder<AppAuditDbContext>()
                .UseSqlServer(Configuration.GetConnectionString("AuditConnection"))
                .Options;

            Audit.Core.Configuration.Setup()
                .UseEntityFramework(x => x
                .UseDbContext<AppAuditDbContext>(auditDbContextOptions)
                .AuditTypeNameMapper(typeName =>
                {
                    return typeName;
                }).AuditEntityAction<IAuditableEntity>((ev, ent, auditEntity) =>
                {
                    var entityFrameworkEvent = ev.GetEntityFrameworkEvent();
                    if (entityFrameworkEvent == null) return;

                    auditEntity.AuditTransactionId = entityFrameworkEvent.TransactionId;
                    auditEntity.AuditAmbientTransactionId = entityFrameworkEvent.AmbientTransactionId;
                    auditEntity.AuditAction = ent.Action;
                }));

            #endregion


            services.AddControllersWithViews();
        }

        // other stuff..
    }
Run Code Online (Sandbox Code Playgroud)

这是经过审核的上下文。

[AuditDbContext(IncludeEntityObjects = true)]
    public class ApplicationDbContext : AuditDbContext
    {

        public ApplicationDbContext([NotNullAttribute] DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Scope> Scopes { get; set; }
        public DbSet<Job> Jobs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Scope>().ToTable("Scope");

            modelBuilder.Entity<Job>().ToTable("Job");
        }

        public override int SaveChanges()
        {
            return base.SaveChanges();
        }

        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            return await base.SaveChangesAsync(cancellationToken);
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是范围的删除控制器操作。

public class ScopeController : Controller
    {
        private readonly ApplicationDbContext _context;

        public ScopeController(ApplicationDbContext context)
        {
            _context = context;
        }

        // Other controller actions...

        // POST: Scope/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var scope = await _context.Scopes.Include(s => s.Jobs).SingleOrDefaultAsync(w => w.Id == id);            
            // using _context.Scopes.FindAsync(id) instead does delete the children Jobs without auditing it
            _context.Scopes.Remove(scope);
            await _context.SaveChangesAsync().ConfigureAwait(false);
            return RedirectToAction(nameof(Index));
        }
    }
Run Code Online (Sandbox Code Playgroud)

此控制器操作从 EF 的角度工作。它还审核父项和子项删除操作,但我不知道如何将子项审核记录与父项审核记录相关联我是否应该在代码中的某处添加一个 AuditScope?请问,我如何配置 Audit.NET 以便能够查询审计数据库以获取分组审计数据?

这是 ID 为 #5 的作用域的审计跟踪。
Scope 的审计追踪
Audit_Scope 表

这是具有 ScopeId #5 的作业的审计跟踪。
作业的审计跟踪
Audit_Job 表

鉴于提供的数据,假设我想读取范围的删除审计(在本例中为 Audit_Scope 表中的 AuditId #9),包括其子作业的删除审计(在本例中为 Audit_Job 表中的 AuditId #10) . 我怎样才能做到这一点?

谢谢,马特奥

m.p*_*bos 2

目前,我刚刚向我的实体添加了一个自定义字段。我在带有 Guid 的自定义操作中重视它。

// EF AuditEventId per scope
Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
    var id = Guid.NewGuid();
    scope.SetCustomField("AuditScopeId", id);
});
Run Code Online (Sandbox Code Playgroud)

这样,与同一审计事件相关的表记录ScopeJob表记录将保持相同的AuditScopeId值。