C#中的自动审计字段(Audit trail)

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的判断中,只包含了AddedModified

  public enum EntityState{        Detached,        Unchanged,        Deleted,        Modified,        Added    }

同时再补充一句,在优化EF查询的时候,由于EF的Change Tracking的机制。建议在只查询数据时候,加上AsNoTracking()的方法,可以提高响应的查询效率。