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

如何用Audit.EntityFramework将审计日志中外键替换为关联实体属性?

问题描述

在使用Audit.EntityFramework时,我希望将关联实体的名称填充到EventEntryChange对象的OriginalValue、NewValue属性,以及ColumnValues对象中。尝试通过Format配置实现:

Audit.EntityFramework.Configuration.Setup()
    .ForContext<AppDbContext>(config => config
        .ForEntity<Product>(prodConfig =>
        {
            prodConfig.Format(x => x.ProductType, value =>
            {
                // 从关联对象获取名称
                var name = relatedObject?.Name;
                return name ?? value;
            });
        })
    );

但发现OriginalValue被替换为名称后,NewValue和ColumnValues又被重置回外键ID。原因是DbContextHelper中的UpdateAuditEvent方法会在EF保存操作完成后,重新更新审计事件的列值和外键:

/// <summary>
/// Updates column values and primary keys on the Audit Event after the EF save operation completes.
/// </summary>
public void UpdateAuditEvent(EntityFrameworkEvent efEvent, IAuditDbContext context)
{
    // Update PK and FK
    foreach (var efEntry in efEvent.Entries)
    {
        var entry = efEntry.Entry;
        efEntry.PrimaryKey = GetPrimaryKey(context.DbContext, entry);
        foreach (var pk in efEntry.PrimaryKey)
        {
            if (efEntry.ColumnValues.ContainsKey(pk.Key))
            {
                efEntry.ColumnValues[pk.Key] = pk.Value;
            }
        }
        var fks = GetForeignKeys(context.DbContext, entry);
        foreach (var fk in fks)
        {
            if (efEntry.ColumnValues.ContainsKey(fk.Key))
            {
                efEntry.ColumnValues[fk.Key] = fk.Value;
            }

            var change = efEntry.Changes?.FirstOrDefault(e => e.ColumnName == fk.Key);
            if (change != null)
            {
                change.NewValue = fk.Value;
            }
        }
    }
    // Update ConnectionId
    var clientConnectionId = TryGetClientConnectionId(context.DbContext);
    if (clientConnectionId != null)
    {
        efEvent.ConnectionId = clientConnectionId;
    }
}
解决方案

可以通过两种方式实现需求:

1. 利用OnSaved事件二次修改审计内容

既然UpdateAuditEvent会在保存后重置外键值,我们可以在审计事件的OnSaved阶段重新替换成关联实体名称:

Audit.EntityFramework.Configuration.Setup()
    .ForContext<AppDbContext>(config => config
        .OnSaved(evt =>
        {
            foreach (var entry in evt.EntityFrameworkEvent.Entries)
            {
                if (entry.EntityType == typeof(Product).Name)
                {
                    var product = evt.GetEntityObject<Product>();
                    var productType = evt.DbContext.Set<ProductType>().Find(product.ProductTypeId);
                    
                    // 修改ColumnValues中的外键值为关联名称
                    if (entry.ColumnValues.ContainsKey("ProductTypeId"))
                    {
                        entry.ColumnValues["ProductTypeId"] = productType?.Name ?? entry.ColumnValues["ProductTypeId"];
                    }
                    
                    // 修改Changes中的OriginalValue和NewValue
                    var typeChange = entry.Changes?.FirstOrDefault(c => c.ColumnName == "ProductTypeId");
                    if (typeChange != null)
                    {
                        // 替换原始值为对应名称
                        if (typeChange.OriginalValue is int originalId)
                        {
                            var originalType = evt.DbContext.Set<ProductType>().Find(originalId);
                            typeChange.OriginalValue = originalType?.Name ?? originalId;
                        }
                        // 替换新值为对应名称
                        typeChange.NewValue = productType?.Name ?? typeChange.NewValue;
                    }
                }
            }
        })
    );

该方法在EF保存完成后,重新覆盖被框架重置的字段值,确保最终审计日志显示关联实体名称。

2. 添加自定义字段存储关联实体名称(推荐)

如果不想修改框架默认的ColumnValues和Change对象,可以在审计事件的CustomFields中新增字段存储关联名称,避免冲突:

Audit.EntityFramework.Configuration.Setup()
    .ForContext<AppDbContext>(config => config
        .ForEntity<Product>(prodConfig =>
        {
            prodConfig.AuditCustomFields((entry, product) => new Dictionary<string, object>
            {
                ["ProductTypeName"] = product.ProductType?.Name ?? "Unknown"
            });
        })
        // 针对更新操作,添加原始关联名称
        .OnSaving(evt =>
        {
            foreach (var entry in evt.EntityFrameworkEvent.Entries)
            {
                if (entry.EntityType == typeof(Product).Name && entry.Action == "Update")
                {
                    var originalProduct = entry.GetOriginalEntityObject<Product>();
                    var originalType = evt.DbContext.Set<ProductType>().Find(originalProduct.ProductTypeId);
                    entry.CustomFields["OriginalProductTypeName"] = originalType?.Name ?? "Unknown";
                }
            }
        })
    );

这种方式不干扰框架默认逻辑,同时能在审计日志中清晰展示关联实体的名称信息,维护性更强。

内容的提问来源于stack exchange,提问作者Charles Boyung

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 10:55:29