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

188 阅读3分钟

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()的方法,可以提高响应的查询效率。

参考

  1. EF-性能提升之AsNoTracking
  2. blog.csdn.net/qq_19922839…