C#中的自动审计字段(Audit trail)
在系统开发的数据库设计中,有时基于对数据变更的追溯,需要添加几个用于审计的字段。如下:
-
IsValid
, 当前是否有效,用于数据行的逻辑删除。 -
CreatedBy
, 数据行的创建人。 -
CreatedTime
, 数据行创建的时间,默认当前时间。 -
UpdatedBy
,数据行的更新人,默认为创建人。 -
UpdatedTime
, 数据行的更新时间,默认为第一次创建时间,后变更时候更新。.
对应的数据库SQL脚本如下:
CREATE TABLE TestTable(
ID int NOT NULL IDENTITY(1, 1),
-- ... (其它 字段)
IsValid bit NOT NULL,
CreatedBy int NOT NULL,
CreatedTime DATETIME NOT NULL,
UpdatedBy int NOT NULL,
UpdatedTime DATETIME NOT NULL,
PRIMARY KEY (ID)
);
以上几个字段,如果在业务对象的操作中手动去设置相应的值,实际上是比较繁琐的,且是容易遗漏的。例如如下的代码,写多了,其实也没太多的意义:
baseEntity.IsValid = true;
// 其它业务字段的赋值。
baseEntity.CreatedByUserId = User.UserId;
baseEntity.CreatedTime = DateTime.Now;
baseEntity.UpdatedByUserId = User.UserId;
baseEntity.UpdatedTime = DateTime.Now;
基于这种场景,我们需要思考,是否有更加优雅的方式,来解决这样的一个问题呢? 是的,Entity Framework是内置了一种Change Tracking
的机制,它会自动跟踪上下文中已加载的实体,并且提供ChangeTracker
类,来方便获取当前实体所有的跟踪信息。所以,我们可以在数据库的SaveChanges()
方法时,进行重写,补上我们需要的审计字段信息逻辑。 以下有两种重新的方式,一种是类有父类时,可以直接通过BaseEntity
的属性进行赋值;另外一种是EF生成的对象,没有继承父类,那么便通过反射判断字段名的方式进行取属性赋值。同时考虑到EF脚手架DB First
在生成实体对象时,会自动生成一个DBContext,所以我们的方法最好通过partial class
的方式单独写出来,防止被EF的覆盖。对应类的代码整理如下,以供参考:
public partial class TestDBContext : DbContext
{
private readonly string _conStr;
public TestDBContext(string conStr)
{
_conStr = conStr;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(_conStr);
}
}
// 业务上也只是会逻辑删除,所以只Track新增和变更的情况
public int SaveChangesWithAuditInfo1(TokenUser tokenUser)
{
var changeTracker = ChangeTracker.Entries().Where(a => a.State == EntityState.Added || a.State == EntityState.Modified);
try
{
foreach (EntityEntry entry in changeTracker)
{
BaseEntity baseEntity = entry.Entity as BaseEntity;
switch (entry.State)
{
case EntityState.Added:
baseEntity.IsValid = true;
baseEntity.CreatedByUserId = tokenUser.UserId;
baseEntity.CreatedTime = DateTime.Now;
baseEntity.UpdatedByUserId = tokenUser.UserId;
baseEntity.UpdatedTime = DateTime.Now;
break;
case EntityState.Modified:
baseEntity.UpdatedByUserId = tokenUser.UserId;
baseEntity.UpdatedTime = DateTime.Now;
break;
default:
break;
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Log.Error(e, "Db SaveChanges Error.");
throw;
}
return base.SaveChanges();
}
// 通过反射, 判断属性,并赋值
public int SaveChangesWithAuditInfo2(TokenUser tokenUser)
{
var changeTracker = ChangeTracker.Entries().Where(a =>
a.State == EntityState.Added || a.State == EntityState.Modified);
try
{
foreach (EntityEntry entry in changeTracker)
{
var dbModelEntity = entry.Entity;
var objectType = dbModelEntity.GetType();
var allProperties = objectType.GetProperties();
switch (entry.State)
{
case EntityState.Added:
if (allProperties.Any(a => a.Name.ToUpper() == "IsValid".ToUpper()))
{
dbModelEntity.GetType().GetProperty("IsValid").SetValue(dbModelEntity, true);
}
if (allProperties.Any(a => a.Name.ToUpper() == "CreatedByUserId".ToUpper()))
{
dbModelEntity.GetType().GetProperty("CreatedByUserId").SetValue(dbModelEntity, tokenUser.UserId);
}
if (allProperties.Any(a => a.Name.ToUpper() == "CreatedTime".ToUpper()))
{
dbModelEntity.GetType().GetProperty("CreatedTime").SetValue(dbModelEntity, DateTime.Now);
}
if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedByUserId".ToUpper()))
{
dbModelEntity.GetType().GetProperty("UpdatedByUserId").SetValue(dbModelEntity, tokenUser.UserId);
}
if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedTime".ToUpper()))
{
dbModelEntity.GetType().GetProperty("UpdatedTime").SetValue(dbModelEntity, DateTime.Now);
}
break;
case EntityState.Modified:
if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedByUserId".ToUpper()))
{
dbModelEntity.GetType().GetProperty("UpdatedByUserId").SetValue(dbModelEntity, tokenUser.UserId);
}
if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedTime".ToUpper()))
{
dbModelEntity.GetType().GetProperty("UpdatedTime").SetValue(dbModelEntity, DateTime.Now);
}
break;
default:
break;
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Log.Error(e, "Db SaveChanges Error.");
throw;
}
return base.SaveChanges();
}
}
最后,在EF ChangeTracker中的EntityState总计有5种,由于我们的数据只会逻辑删除,不会有Deleted
的状态,所以在SaveChanges的判断中,只包含了Added
和Modified
:
public enum EntityState
{
Detached,
Unchanged,
Deleted,
Modified,
Added
}
同时再补充一句,在优化EF查询的时候,由于EF的Change Tracking
的机制。建议在只查询数据时候,加上AsNoTracking()
的方法,可以提高响应的查询效率。