EF审计追踪:反射实现的性能问题及最佳实践咨询
嘿,这个问题问到点子上了——在EF Core里处理审计追踪字段时,反射的性能开销、EntityEntry.Property的底层实现,还有更优的替代方案,确实是很多开发者都会踩的坑。我来给你逐一拆解:
首先明确:反射确实会带来一定的性能开销,但具体影响要看你的使用方式:
- 性能方面:每次调用
Type.GetProperty()这类反射方法时,CLR都需要在类型的元数据中查找对应的属性信息,这个过程会比直接访问强类型属性慢。如果你的系统是高并发、高频读写的场景,无缓存的重复反射会累积出明显的性能损耗。 - 内存方面:反射操作会生成一些临时的元数据对象,如果没有缓存这些结果,每次反射都会重复创建这些对象,增加GC的压力。
不过也不用过度焦虑——如果只是偶尔调用或者做了缓存,反射的开销其实可以忽略不计。
答案是:第一次调用时会用反射,但后续调用会复用缓存的元数据。
EF Core内部维护了模型的元数据缓存,当你第一次通过字符串名称调用Property()时,它会用反射查找该属性的信息,然后把这个信息缓存起来。之后再调用同一个实体的同一个属性时,就直接从缓存里取,不会再走反射了。
所以相比你自己手动写无缓存的反射代码,EntityEntry.Property()的性能要好很多,因为它利用了EF Core的内置缓存机制。
这里给你几个优先级从高到低的方案,都是生产环境验证过的:
方案一:使用强类型审计接口(性能最优,无反射)
定义一个包含所有审计字段的接口,让需要审计的实体实现这个接口,然后在SaveChanges()里直接强类型处理:
// 定义审计接口 public interface IAuditableEntity { string InsertedBy { get; set; } DateTime InsertedDate { get; set; } string UpdatedBy { get; set; } DateTime UpdatedDate { get; set; } } // 在你的DbContext里重写SaveChanges public override int SaveChanges(bool acceptAllChangesOnSuccess) { var currentUser = GetCurrentUserName(); // 这里替换成获取当前用户的逻辑 var utcNow = DateTime.UtcNow; // 只处理实现了IAuditableEntity的实体 foreach (var entry in ChangeTracker.Entries<IAuditableEntity>()) { switch (entry.State) { case EntityState.Added: entry.Entity.InsertedBy = currentUser; entry.Entity.InsertedDate = utcNow; entry.Entity.UpdatedBy = currentUser; entry.Entity.UpdatedDate = utcNow; break; case EntityState.Modified: entry.Entity.UpdatedBy = currentUser; entry.Entity.UpdatedDate = utcNow; // 可选:防止修改创建时间和创建人 entry.Property(e => e.InsertedBy).IsModified = false; entry.Property(e => e.InsertedDate).IsModified = false; break; } } return base.SaveChanges(acceptAllChangesOnSuccess); }
这个方案完全不需要反射,性能拉满,而且代码可读性极强,是最推荐的做法。
方案二:缓存反射元数据(适合无法修改实体的场景)
如果因为某些原因不能给实体加接口(比如第三方实体类),可以提前缓存反射得到的属性信息,避免每次SaveChanges都重复反射:
public class YourDbContext : DbContext { // 缓存实体类型对应的审计属性 private static readonly Dictionary<Type, (PropertyInfo InsertedBy, PropertyInfo InsertedDate, PropertyInfo UpdatedBy, PropertyInfo UpdatedDate)> _auditPropsCache = new(); public YourDbContext(DbContextOptions<YourDbContext> options) : base(options) { // 初始化缓存:在上下文第一次创建时,扫描所有实体的审计属性 foreach (var entityType in Model.GetEntityTypes()) { var clrType = entityType.ClrType; var insertedByProp = clrType.GetProperty(nameof(IAuditableEntity.InsertedBy)); var insertedDateProp = clrType.GetProperty(nameof(IAuditableEntity.InsertedDate)); var updatedByProp = clrType.GetProperty(nameof(IAuditableEntity.UpdatedBy)); var updatedDateProp = clrType.GetProperty(nameof(IAuditableEntity.UpdatedDate)); if (insertedByProp != null && insertedDateProp != null && updatedByProp != null && updatedDateProp != null) { _auditPropsCache[clrType] = (insertedByProp, insertedDateProp, updatedByProp, updatedDateProp); } } } public override int SaveChanges(bool acceptAllChangesOnSuccess) { var currentUser = GetCurrentUserName(); var utcNow = DateTime.UtcNow; foreach (var entry in ChangeTracker.Entries()) { if (_auditPropsCache.TryGetValue(entry.Entity.GetType(), out var props)) { if (entry.State == EntityState.Added) { props.InsertedBy.SetValue(entry.Entity, currentUser); props.InsertedDate.SetValue(entry.Entity, utcNow); props.UpdatedBy.SetValue(entry.Entity, currentUser); props.UpdatedDate.SetValue(entry.Entity, utcNow); } else if (entry.State == EntityState.Modified) { props.UpdatedBy.SetValue(entry.Entity, currentUser); props.UpdatedDate.SetValue(entry.Entity, utcNow); } } } return base.SaveChanges(acceptAllChangesOnSuccess); } }
这个方案只在上下文初始化时做一次反射,之后所有操作都用缓存的PropertyInfo,性能损耗几乎可以忽略。
方案三:使用EF Core的ChangeTracker事件(更灵活)
你还可以利用EF Core的ChangeTracker.Tracked和ChangeTracker.StateChanged事件,在实体被追踪或者状态变化时自动设置审计字段,结合强类型接口或者缓存元数据来实现,这样可以把审计逻辑和SaveChanges()解耦。
- 无缓存的手动反射会带来可感知的性能和内存损耗,一定要避免;
EntityEntry.Property("InsertedBy")第一次调用用反射,但之后会缓存,比无缓存的手动反射好;- 强类型审计接口是性能最优、可读性最好的方案,优先选用;
- 无法修改实体时,缓存反射元数据是次优选择。
内容的提问来源于stack exchange,提问作者Amir

