如何在EF Core+MySQL环境下为TPT实体实现可靠的属性级审计系统以跟踪完整更新历史
如何在EF Core+MySQL环境下为TPT实体实现可靠的属性级审计系统以跟踪完整更新历史
我完全理解你的处境——用EF Core配合MySQL做TPT继承的报表系统,想要实现属性级的审计追踪,但MySQL不像SQL Server有现成的时态表可用。下面我会给你一套可靠的落地方案,覆盖所有Report派生类的变更记录,包括更新前后的属性值、删除操作,完美适配你的TPT场景。
1. 设计审计日志实体模型
首先我们需要定义一个审计日志实体,用来存储所有变更的核心细节,确保能追溯到每一次操作的完整信息:
public enum AuditChangeType { Update, Delete } [Table("AuditLogs")] public class AuditLog { [Key] public int Id { get; set; } // 关联的数据库表名(TPT场景下区分基表/派生表) [Required, MaxLength(100)] public string TableName { get; set; } = null!; // 被变更的实体ID public int EntityId { get; set; } // 变更类型:更新/删除 public AuditChangeType ChangeType { get; set; } // 被变更的属性名 [Required, MaxLength(100)] public string PropertyName { get; set; } = null!; // 旧值(删除时为实体当前值,更新时为变更前的值) public string? OldValue { get; set; } // 新值(删除时为null,更新时为变更后的值) public string? NewValue { get; set; } // 操作人 [Required, MaxLength(100)] public string ChangedBy { get; set; } = null!; // 操作时间(建议用UTC时间避免时区问题) public DateTime ChangedAt { get; set; } }
2. 实现EF Core保存拦截器
EF Core的SaveChangesInterceptor是实现全局变更追踪的最佳方式,它能在保存操作执行前后拦截并处理实体变更,完美适配TPT继承场景——EF Core的ChangeTracker会自动追踪所有派生类实体的状态和属性变更。
我们自定义拦截器,重写保存方法,在执行保存前捕获所有Report及其派生类的变更:
public class AuditSaveChangesInterceptor : SaveChangesInterceptor { // Web应用中可注入IHttpContextAccessor获取当前登录用户 private readonly IHttpContextAccessor? _httpContextAccessor; public AuditSaveChangesInterceptor(IHttpContextAccessor? httpContextAccessor = null) { _httpContextAccessor = httpContextAccessor; } public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { if (eventData.Context is not null) { ProcessAuditLogs(eventData.Context); } return base.SavingChanges(eventData, result); } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { if (eventData.Context is not null) { ProcessAuditLogs(eventData.Context); } return base.SavingChangesAsync(eventData, result, cancellationToken); } private void ProcessAuditLogs(DbContext context) { // 获取当前操作人:可根据业务场景调整(比如从会话、配置中心获取) string GetCurrentUser() { if (_httpContextAccessor?.HttpContext?.User?.Identity?.Name is string username) { return username; } // 非Web场景可替换为固定值或从其他渠道获取 throw new InvalidOperationException("无法获取当前操作人,请检查配置"); } var currentUser = GetCurrentUser(); var currentTime = DateTime.UtcNow; // 拦截所有状态为Modified/Deleted的Report及其派生类实体 foreach (var entityEntry in context.ChangeTracker.Entries<Report>()) { if (entityEntry.State is EntityState.Modified or EntityState.Deleted) { // 自动维护Report的UpdatedDate和UpdatedBy字段 if (entityEntry.State == EntityState.Modified) { entityEntry.Property(nameof(Report.UpdatedDate)).CurrentValue = currentTime; entityEntry.Property(nameof(Report.UpdatedBy)).CurrentValue = currentUser; } var entityId = entityEntry.Property(nameof(Report.Id)).CurrentValue as int? ?? 0; var auditLogs = new List<AuditLog>(); switch (entityEntry.State) { case EntityState.Modified: // 遍历所有有变更的属性 foreach (var propertyEntry in entityEntry.Properties.Where(p => p.IsModified)) { var propertyMetadata = propertyEntry.Metadata; // TPT场景下,准确获取属性所属的表名(基类属性对应Reports,派生类属性对应各自表) var tableName = propertyMetadata.DeclaringType.GetTableName() ?? string.Empty; if (string.IsNullOrEmpty(tableName)) continue; // 序列化属性值为字符串,适配所有数据类型 var oldValue = SerializeValue(propertyEntry.OriginalValue); var newValue = SerializeValue(propertyEntry.CurrentValue); auditLogs.Add(new AuditLog { TableName = tableName, EntityId = entityId, ChangeType = AuditChangeType.Update, PropertyName = propertyMetadata.Name, OldValue = oldValue, NewValue = newValue, ChangedBy = currentUser, ChangedAt = currentTime }); } break; case EntityState.Deleted: // 记录删除操作的所有属性值 foreach (var propertyEntry in entityEntry.Properties) { var propertyMetadata = propertyEntry.Metadata; var tableName = propertyMetadata.DeclaringType.GetTableName() ?? string.Empty; if (string.IsNullOrEmpty(tableName)) continue; var oldValue = SerializeValue(propertyEntry.OriginalValue); auditLogs.Add(new AuditLog { TableName = tableName, EntityId = entityId, ChangeType = AuditChangeType.Delete, PropertyName = propertyMetadata.Name, OldValue = oldValue, NewValue = null, ChangedBy = currentUser, ChangedAt = currentTime }); } break; } // 将审计日志加入上下文,和主操作一起提交 context.Set<AuditLog>().AddRange(auditLogs); } } } // 通用值序列化方法,兼容所有数据类型 private string? SerializeValue(object? value) { if (value == null) return null; if (value.GetType().IsValueType || value is string) { return value.ToString(); } // 复杂类型用JSON序列化 return JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = false }); } }
3. 注册拦截器到DbContext
最后需要把自定义拦截器注册到你的DbContext,ASP.NET Core应用推荐用DI容器注册的方式:
步骤1:在Program.cs中注册服务
builder.Services.AddHttpContextAccessor(); // 注册HttpContextAccessor获取当前用户 builder.Services.AddDbContext<AppDbContext>((sp, options) => { options.UseMySQL(builder.Configuration.GetConnectionString("DefaultConnection")); // 从DI容器获取拦截器依赖并注册 var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>(); options.AddInterceptors(new AuditSaveChangesInterceptor(httpContextAccessor)); });
步骤2:配置你的AppDbContext
public class AppDbContext : DbContext { public DbSet<Report> Reports => Set<Report>(); public DbSet<QualityReport> QualityReports => Set<QualityReport>(); public DbSet<MaintenanceReport> MaintenanceReports => Set<MaintenanceReport>(); public DbSet<SafetyReport> SafetyReports => Set<SafetyReport>(); public DbSet<AuditLog> AuditLogs => Set<AuditLog>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // 显式配置派生类的表名(和TPT策略匹配) modelBuilder.Entity<QualityReport>().ToTable("QualityReports"); modelBuilder.Entity<MaintenanceReport>().ToTable("MaintenanceReports"); modelBuilder.Entity<SafetyReport>().ToTable("SafetyReports"); } }
4. 关键细节说明
- TPT场景完美适配:通过EF Core元数据
propertyMetadata.DeclaringType.GetTableName(),能准确区分基类属性(对应Reports表)和派生类属性(对应各自表),审计日志和实际数据库操作完全对齐。 - 自动维护实体字段:拦截器中自动更新
UpdatedDate和UpdatedBy,避免业务代码重复编写,同时这些字段的变更也会被记录到审计日志(如果不需要可在代码中排除)。 - 灵活的操作人获取:支持Web场景从
HttpContext获取用户,非Web场景可直接修改GetCurrentUser方法适配你的业务逻辑。 - 全类型值序列化:通用序列化方法支持所有数据类型,包括值类型、字符串和复杂对象,确保审计日志的完整性。
内容来源于stack exchange




