ef core 无内置审计日志,需通过重写 savechanges 或 isavechangesinterceptor 实现;前者轻量但易递归,后者更安全需同步/异步双实现;字段差异须手动比对 originalvalue/currentvalue,敏感字段应过滤;时态表与应用层审计互补而非替代。

EF Core 本身不提供开箱即用的审计日志功能,但通过重写 SaveChanges 或使用 ISaveChangesInterceptor,你可以在数据写入前自动捕获所有增删改操作及字段级差异——这是最轻量、最可控、也最容易出错的实现路径。
重写 SaveChanges 是最直接的方式,但要注意递归调用
在自定义 DbContext 中重写 SaveChanges 方法,是最常见也最容易上手的做法。核心逻辑是遍历 ChangeTracker.Entries(),筛选出 Added、Modified、Deleted 状态的实体,构造 AuditLog 实体并加入当前上下文。
- 必须跳过
AuditLog自身的变更,否则会无限递归触发审计(判断条件:entry.Entity is AuditLog) - 主键值不能硬编码为
Id,要用entry.Metadata.FindPrimaryKey()?.Properties获取真实主键属性 - 添加日志实体时,用
Entry(log).State = EntityState.Added而非Add(log),避免再次触发SaveChanges拦截 -
OriginalValues只在SaveChanges调用前有效;保存后它会被同步为CurrentValues,所以别在SaveChangesAsync完成后再去读
ISaveChangesInterceptor 更安全,但需注意同步/异步一致性
从 EF Core 5.0 开始,ISaveChangesInterceptor 是官方推荐的拦截方式,它天然解耦了审计逻辑与 DbContext 生命周期,也更利于单元测试。
- 必须同时实现
SavingChanges(同步)和SavingChangesAsync(异步),否则混合调用时可能漏记录 - 获取当前用户不能依赖
IHttpContextAccessor的HttpContext.User在后台服务中会为空,建议提前将UserId注入拦截器构造函数 - 不要在拦截器里做耗时操作(如查数据库、调外部 API),否则会拖慢所有写操作;如真需要,应改用
SaveChangesAsync并确保日志写入也是异步的 - 拦截器实例默认是单例,必须是无状态的;所有上下文相关数据(如
DbContext、用户 ID)都应从DbContextEventData或构造参数传入
字段级差异提取容易踩坑:IsModified 不等于值真的变了
property.IsModified 返回 true,只代表该属性被显式标记为修改(比如调用了 entry.Property(x => x.Name).IsModified = true),不代表它的 OriginalValue 和 CurrentValue 真的不等——EF Core 不做值比较,只看跟踪标记。
- 手动调用
entry.Property("Name").CurrentValue = entry.Property("Name").OriginalValue后,IsModified仍为true - 正确做法是显式比对:
!Equals(property.OriginalValue, property.CurrentValue) - 注意
null值比较,Equals(null, null)返回true,但null == null也成立;用object.Equals更稳妥 - 敏感字段(如密码哈希、Token)应在审计前过滤掉,可用自定义特性
[AuditIgnore]或白名单配置控制
时态表不是审计日志的替代方案,而是互补手段
SQL Server 的时态表(Temporal Table)由数据库原生支持,能自动保存历史快照,但它记录的是“行级完整副本”,不是“字段级变更摘要”。它解决的是“某个时间点这条记录长什么样”,而不是“谁在什么时候改了哪个字段”。
- 时态表无法记录操作人、IP、业务语义(如“审核通过”还是“驳回”),这些仍需应用层审计日志补充
- 启用时态表后,
ChangeTracker仍能正常工作,两者可共存:时态表兜底历史数据,应用审计日志提供上下文和语义 - EF Core 配置时态表需显式指定
PeriodStart和PeriodEnd字段,且这两个字段不能是DateTimeOffset,必须是DateTime(SQL Server 限制) - 历史表名默认为
{TableName}History,可通过UseHistoryTable("MyAuditLog")自定义,但该表结构不可手动修改,否则时态功能失效
真正难的不是写出第一版审计逻辑,而是让这套机制在高并发、多租户、混合调用(同步/异步、Web/Worker)、字段动态变化的场景下稳定不出错——尤其是主键类型不统一(int、Guid、string)、软删除实体混入、以及跨上下文共享日志上下文这些边界情况,往往要到压测或上线后才暴露。











