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

Audit.NET Entity Framework适配器:嵌套属性未被记录问题

Audit.NET嵌套属性审计字段为空问题排查与解决

问题描述

已为项目中的ElementoWorkflow模型配置Audit.NET,整体审计功能正常,但嵌套属性StatoOrigine对应的审计表字段ID_STATO_ORIGINE_007始终为Null。相关代码、配置及数据库结构如下:

模型代码

public class ElementoWorkflow : BaseObject<ElementoWorkflow, int>
{
    public override int Id { get;  set; }
    public required Stato StatoOrigine { get;  set; }
    public required bool Finale { get; set; } = false;
}

public class Stato : ServiceRegistryObject
{
    public override RegistryType Gruppo {
        get => RegistryType.Stato;
        set => throw new InvalidOperationException("Non è possibile modificare il Gruppo di appartenenza. Utilizzare la classe generica ServiceRegistryObject.");
    }
}

public class ServiceRegistryObject : ServiceBaseObject<ServiceRegistryObject, int>
{
    public override int Id { get; set; }
    public virtual string? Codice { get; set; }
    public virtual RegistryType Gruppo { get; set; }
}

public abstract class ServiceBaseObject<TEntity, TKey> : BaseObject<TEntity, TKey>
where TEntity : class
{
    public abstract bool Annullato { get; set; }

    public abstract TKey Ordine { get; set; }
}

public void Configure(EntityTypeBuilder<ElementoWorkflow> builder)
{
    builder
        .ToTable($"NAT_DASH_ELEMENTO_WORKFLOW_007")
        .HasKey(c => c.Id)
        .HasName("NAK_DASH_007");

    builder.Property(b => b.Id)
        .HasColumnName("ID_007");
    builder.Property<int>("IdStatoOrigine")
        .HasColumnName("ID_STATO_ORIGINE_007")
        .IsRequired();
    builder.HasOne(c => c.StatoOrigine)
        .WithMany()
        .HasForeignKey("IdStatoOrigine")
        .HasPrincipalKey(g => g.Id)
        .IsRequired();      
    builder.Property(b => b.Finale)
        .HasColumnName("FINALE_007")
        .IsRequired();
}

Audit.NET配置代码

public DatabaseContext(DbContextOptions<DatabaseContext> options, ILoggerFactory loggerFactory, IHttpContextAccessor httpContextAccessor)
 : base(options)
{
    _loggerFactory = loggerFactory;
    this.ChangeTracker.LazyLoadingEnabled = false;

    Audit.EntityFramework.Configuration.Setup()
        .ForContext<DatabaseContext>(config => config
            .IncludeEntityObjects()     
            .ReloadDatabaseValues(true)
            .AuditEventType("DatabaseContext"))
        
        .UseOptOut()
            .IgnoreAny(t => t.Name.EndsWith("History"));


    Audit.Core.Configuration.Setup()
        .UseEntityFramework(ef => ef
        .AuditTypeExplicitMapper(m => m
        .Map<ElementoWorkflow, ElementoWorkflowAudit>()
        .AuditEntityAction<IAudit>((evt, entry, auditEntity) =>
        {
            auditEntity.AuditData = DateTime.UtcNow;
            auditEntity.AuditUser = httpContextAccessor.HttpContext?.User.Claims.FirstOrDefault(c => c.Type.Contains(TipoClaim.matricola.ToString()))?.Value;
            auditEntity.AuditAction = entry.Action;
        })
    )
);

审计实体及配置代码

internal class ElementoWorkflowAudit : IAudit
{
    public int AuditId { get; set; }
    public string AuditAction { get; set; }
    public string? AuditUser { get; set; }
    public DateTime AuditData { get; set; }
    public int Id { get; set; }
    public Stato StatoOrigine { get; set; }
    public bool Finale { get; set; } = false;
}

public void Configure(EntityTypeBuilder<ElementoWorkflowAudit> builder)
{
    builder
         .ToTable("NAT_DASH_AUD_ELEMENTO_WORKFLOW_007")
        .HasKey(e => e.AuditId)
        .HasName("NAK_DASH_AUD_ELEMENTO_WORKFLOW_007");
    builder.Property(b => b.AuditId).HasColumnName("AuditId")
        .HasDefaultValueSql($"SYUNA{Costanti.CodiceApplicazione}.SEQ_{Costanti.CodiceApplicazione}__AUD.NEXTVAL");
    builder.Property(b => b.AuditData);
    builder.Property(b => b.AuditAction);
    builder.Property(b => b.AuditUser);

    builder.Property(b => b.Id)
        .HasColumnName("ID_007");

    builder.Property<int>("IdStatoOrigine")
        .HasColumnName("ID_STATO_ORIGINE_007");

    builder.HasOne(c => c.StatoOrigine)
        .WithMany()
        .HasForeignKey("IdStatoOrigine")
        .HasPrincipalKey(g => g.Id);

    builder.Property(b => b.Finale)
        .HasColumnName("FINALE_007");
}

Oracle数据库审计表结构

CREATE TABLE NAT_DASH_AUD_ELEMENTO_WORKFLOW_007 
(
  "AuditId" NUMBER(9, 0) DEFAULT "SYUNADASH"."SEQ_DASH__AUD"."NEXTVAL" NOT NULL 
, "AuditAction" VARCHAR2(250 BYTE) 
, "AuditUser" VARCHAR2(250 BYTE) 
, "AuditData" DATE 
, ID_007 NUMBER(12, 0) 
, ID_STATO_ORIGINE_007 NUMBER(9, 0) 
, FINALE_007 NUMBER(1, 0) 
, CONSTRAINT NAK_DASH_AUD_ELEMENTO_WORKFLOW_007 PRIMARY KEY 
  (
    "AuditId" 
  )
)

原因分析

  1. 影子属性未自动同步:原实体与审计实体均使用影子属性IdStatoOrigine存储外键,但Audit.NET默认不会自动将原实体的影子属性值同步到审计实体的对应字段。
  2. 导航属性未加载:由于ChangeTracker.LazyLoadingEnabled = false,若操作时未显式加载StatoOrigine导航属性,Audit.NET无法获取关联实体的ID,导致审计外键字段为空。

解决方案

方案一:手动填充审计实体的影子属性

在AuditEntityAction中,从原实体的影子属性或已加载的导航属性中获取外键值,手动设置到审计实体的影子属性:

.AuditEntityAction<IAudit>((evt, entry, auditEntity) =>
{
    auditEntity.AuditData = DateTime.UtcNow;
    auditEntity.AuditUser = httpContextAccessor.HttpContext?.User.Claims.FirstOrDefault(c => c.Type.Contains(TipoClaim.matricola.ToString()))?.Value;
    auditEntity.AuditAction = entry.Action;
    
    var elementoWorkflow = entry.Entity as ElementoWorkflow;
    if (elementoWorkflow != null)
    {
        // 优先从原实体影子属性获取值,无需依赖导航属性加载
        var idStatoOrigine = entry.Context.Entry(elementoWorkflow).Property("IdStatoOrigine").CurrentValue;
        entry.Context.Entry(auditEntity).Property("IdStatoOrigine").CurrentValue = idStatoOrigine;
    }
})

方案二:将审计实体的影子属性改为显式属性

修改ElementoWorkflowAudit添加显式外键属性,Audit.NET会自动匹配原实体的外键值,无需手动处理:

internal class ElementoWorkflowAudit : IAudit
{
    public int AuditId { get; set; }
    public string AuditAction { get; set; }
    public string? AuditUser { get; set; }
    public DateTime AuditData { get; set; }
    public int Id { get; set; }
    // 添加显式外键属性
    public int IdStatoOrigine { get; set; }
    public Stato StatoOrigine { get; set; }
    public bool Finale { get; set; } = false;
}

同步更新审计实体的EF配置,绑定显式属性作为外键:

// 替换原影子属性配置
builder.Property(b => b.IdStatoOrigine)
    .HasColumnName("ID_STATO_ORIGINE_007");

builder.HasOne(c => c.StatoOrigine)
    .WithMany()
    .HasForeignKey(c => c.IdStatoOrigine)
    .HasPrincipalKey(g => g.Id);

方案三:启用Audit.NET导航属性包含配置

在Audit.NET的EF上下文配置中添加.IncludeNavigationProperties(),自动处理导航属性及关联外键(注意:会加载关联实体,可能影响性能):

Audit.EntityFramework.Configuration.Setup()
    .ForContext<DatabaseContext>(config => config
        .IncludeEntityObjects()     
        .ReloadDatabaseValues(true)
        .IncludeNavigationProperties()
        .AuditEventType("DatabaseContext"))

推荐方案

优先选择方案二,显式外键属性的方式更直观,Audit.NET的映射逻辑可自动处理,避免手动维护影子属性的繁琐,同时降低导航属性未加载导致的问题。

内容的提问来源于stack exchange,提问作者gt.guybrush

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 22:38:08