使用实体框架核心创建简单的审计跟踪

目录

介绍

背景

审核日志数据

审核表实体

审核类型

审计Db上下文

审核表配置

数据更改到审核表

实体更改为审核表实体

所有实体更改为审计表

在现有的DbContext中使用审核跟踪

创建DbContext

db:SQL服务器

创建对象

删除对象

使用DbContext

解决方案和项目

参考文献

局限性

下一步是什么?


介绍

在一个特定的项目中,我们必须记录任何最终用户所做的数据更改。这需要在不对现有解决方案进行很多代码更改的情况下完成。该项目使用的是Entity Framework,所以我想为什么不在SaveChanges()方法内部完成。

背景

该数据库是SQL ServerORM实体框架核心(EF Core),并且该应用程序使用的是自定义SaveChanges(string userName)方法,而不是常规SaveChanges()方法。因此,我们决定在该方法中添加内容。另外,这是一个优势,因为我们可以在该方法中获取审核员姓名。

这是日志表示例:

让我们开始编码。

审核日志数据

审核表实体

该实体将用作数据库日志表。

using System;
using System.Collections.Generic;
using System.Text;

namespace Db.Table
{
    public class Audit
    {
        public Guid Id { get; set; }                    /*Log id*/
        public DateTime AuditDateTimeUtc { get; set; }  /*Log time*/
        public string AuditType { get; set; }           /*Create, Update or Delete*/
        public string AuditUser { get; set; }           /*Log User*/
        public string TableName { get; set; }           /*Table where rows been 
                                                          created/updated/deleted*/
        public string KeyValues { get; set; }           /*Table Pk and it's values*/
        public string OldValues { get; set; }           /*Changed column name and old value*/
        public string NewValues { get; set; }           /*Changed column name 
                                                          and current value*/
        public string ChangedColumns { get; set; }      /*Changed column names*/
    }
}
  • Id:日志ID或日志表主键
  • AuditDateTimeUtc:以UTC记录日期时间
  • AuditType:创建/更新/删除
  • AuditUser:用户更改的数据
  • TableName:创建/更新/删除行的表
  • KeyValues:更改了行的主键值和列名(JSON字符串)
  • OldValues:更改了行的旧值和列名(JSON字符串,仅更改了列)
  • NewValues:更改了行的当前/新值和列名(JSON字符串,仅更改了列)
  • ChangedColumns:更改了行的列名(JSON字符串,仅更改了列)

审核类型

using System;
using System.Collections.Generic;
using System.Text;

namespace Db.Status
{
    public enum AuditType
    {
        None = 0,
        Create = 1,
        Update = 2,
        Delete = 3
    }
}
  • Create:将新行添加到表中
  • Update:现有行已修改
  • Delete:现有行已删除

审计Db上下文

创建一个接口以为实体框架指定基于审计跟踪的数据库上下文:

using Db.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace Db
{
    public interface IAuditDbContext
    {
        DbSet<Audit> Audit { get; set; }
        ChangeTracker ChangeTracker { get; }
    }
}
  • DbSet<Audit> Audit { get; set; } 是审核日志表。
  • ChangeTracker ChangeTracker { get; }DbContext默认属性,我们将使用它来跟踪更改详细信息。

审核表配置

根据需要创建一个实体到表映射器配置。如果我们在不使用任何表配置类的情况下首先进行代码编写,则这是可选的。

using Db.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Db.Configuration
{
    internal class AuditConfig : IEntityTypeConfiguration<Audit>
    {
        public void Configure(EntityTypeBuilder<Audit> entity)
        {
            entity.HasKey(e => e.Id);

            entity.ToTable("tbl_test_audit_trail");

            entity.Property(e => e.Id)
                .HasColumnName("id");
            entity.Property(e => e.AuditDateTimeUtc)
                .HasColumnName("audit_datetime_utc");
            entity.Property(e => e.AuditType)
                .HasColumnName("audit_type");
            entity.Property(e => e.AuditUser)
                .HasColumnName("audit_user");        
            entity.Property(e => e.TableName)
                .HasColumnName("table_name");
            entity.Property(e => e.KeyValues)
                .HasColumnName("key_values");
            entity.Property(e => e.OldValues)
                .HasColumnName("old_values");
            entity.Property(e => e.NewValues)
                .HasColumnName("new_values");
            entity.Property(e => e.ChangedColumns)
                .HasColumnName("changed_columns");
        }
    }
}

数据更改到审核表

实体更改为审核表实体

创建一个帮助程序类以映射来自数据库实体的所有数据更改,并使用这些更改信息创建Audit日志实体。在这里,我们使用JSON序列化程序来指定与列值相关的更改。

using Db.Status;
using Db.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Db.Helper.AuditTrail
{
    public class AuditEntry
    {
        public EntityEntry Entry { get; }
        public AuditType AuditType { get; set; }
        public string AuditUser { get; set; }
        public string TableName { get; set; }
        public Dictionary<string, object> 
               KeyValues { get; } = new Dictionary<string, object>();
        public Dictionary<string, object> 
               OldValues { get; } = new Dictionary<string, object>();
        public Dictionary<string, object> 
               NewValues { get; } = new Dictionary<string, object>();
        public List<string> ChangedColumns { get; } = new List<string>();

        public AuditEntry(EntityEntry entry, string auditUser)
        {
            Entry = entry;
            AuditUser = auditUser;
            SetChanges();
        }

        private void SetChanges()
        {
            TableName = Entry.Metadata.Relational().TableName;
            foreach (PropertyEntry property in Entry.Properties)
            {
                string propertyName = property.Metadata.Name;
                string dbColumnName = property.Metadata.Relational().ColumnName;

                if (property.Metadata.IsPrimaryKey())
                {
                    KeyValues[propertyName] = property.CurrentValue;
                    continue;
                }

                switch (Entry.State)
                {
                    case EntityState.Added:
                        NewValues[propertyName] = property.CurrentValue;
                        AuditType = AuditType.Create;
                        break;

                    case EntityState.Deleted:
                        OldValues[propertyName] = property.OriginalValue;
                        AuditType = AuditType.Delete;
                        break;

                    case EntityState.Modified:
                        if (property.IsModified)
                        {
                            ChangedColumns.Add(dbColumnName);

                            OldValues[propertyName] = property.OriginalValue;
                            NewValues[propertyName] = property.CurrentValue;
                            AuditType = AuditType.Update;
                        }
                        break;
                }
            }
        }

        public Audit ToAudit()
        {
            var audit = new Audit();
            audit.Id = Guid.NewGuid();
            audit.AuditDateTimeUtc = DateTime.UtcNow;
            audit.AuditType = AuditType.ToString();
            audit.AuditUser = AuditUser;
            audit.TableName = TableName;
            audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
            audit.OldValues = OldValues.Count == 0 ? 
                              null : JsonConvert.SerializeObject(OldValues);
            audit.NewValues = NewValues.Count == 0 ? 
                              null : JsonConvert.SerializeObject(NewValues);
            audit.ChangedColumns = ChangedColumns.Count == 0 ? 
                                   null : JsonConvert.SerializeObject(ChangedColumns);

            return audit;
        }
    }
}

所有实体更改为审计表

该帮助程序类正在使用以下AuditEntry类:

  • 考虑当前IAuditDbContext所有可能的数据更改来创建Audit日志实体
  • 将日志实体添加到日志表
using Db.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Collections.Generic;
using System.Linq;

namespace Db.Helper.AuditTrail
{
    class AuditHelper
    {
        readonly IAuditDbContext Db;

        public AuditHelper(IAuditDbContext db)
        {
            Db = db;
        }

        public void AddAuditLogs(string userName)
        {
            Db.ChangeTracker.DetectChanges();
            List<AuditEntry> auditEntries = new List<AuditEntry>();
            foreach (EntityEntry entry in Db.ChangeTracker.Entries())
            {
                if (entry.Entity is Audit || entry.State == EntityState.Detached || 
                    entry.State == EntityState.Unchanged)
                {
                    continue;
                }
                var auditEntry = new AuditEntry(entry, userName);
                auditEntries.Add(auditEntry);
            }

            if (auditEntries.Any())
            {
                var logs = auditEntries.Select(x => x.ToAudit());
                Db.Audit.AddRange(logs);
            }
        }
    }
}

在现有的DbContext中使用审核跟踪

让我们通过继承IAuditDbContext创建DbContext对象来创建接口IMopDbContext

using Db.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;

namespace Db
{
    public interface IMopDbContext : IAuditDbContext, IDisposable
    {
        DbSet<Role> Role { get; set; }

        DatabaseFacade Database { get; }
        int SaveChanges(string userName);
    }
}
  • DbSet<Role> Role { get; set; } 是现有的数据表。
  • SaveChanges(string userName内部),我们将使用AuditHelper类来考虑所有实体更改来创建Audit实体。然后,审核实体将被添加到审核跟踪表中。

创建DbContext

在现有/测试数据库环境中,我们将要:

  • 添加审核表DbSet<Audit> Audit { get; set; }
  • OnConfiguring(DbContextOptionsBuilder optionsBuilder)方法中添加审核表配置modelBuilder.ApplyConfiguration(new AuditConfig()),正如我之前提到的那样,这是可选的。
  • 添加SaveChanges(string userName)方法以创建审核日志。
using System;
using Db.Table;
using Db.Configuration;
using Microsoft.EntityFrameworkCore;
using Db.Helper.AuditTrail;

namespace Db
{
    public abstract class MopDbContext : DbContext, IMopDbContext
    {
        public virtual DbSet<Audit> Audit { get; set; }
        public virtual DbSet<Role> Role { get; set; }

        public MopDbContext(DbContextOptions<MopDbContext> options)
            : base(options)
        {
        }

        protected MopDbContext() : base()
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new AuditConfig());
            modelBuilder.ApplyConfiguration(new RoleConfig());
        }

        public virtual int SaveChanges(string userName)
        {
            new AuditHelper(this).AddAuditLogs(userName);
            var result = SaveChanges();
            return result;
        }
    }
}

dbSQL服务器

为了进行测试,我们使用的是MS SQL Server数据库,但是它对任何Db都是有益的。

查找相关的表对象脚本,如下所示。

创建对象

CREATE TABLE [dbo].[tbl_test_audit_trail] (
    [id]                 UNIQUEIDENTIFIER NOT NULL,
    [audit_datetime_utc] DATETIME2        NOT NULL,
    [audit_type]         NVARCHAR (50)    NOT NULL,
    [audit_user]         NVARCHAR (100)   NOT NULL,
    [table_name]         NVARCHAR (150)   NULL,
    [key_values]         NVARCHAR (250)   NULL,
    [old_values]         NVARCHAR (MAX)   NULL,
    [new_values]         NVARCHAR (MAX)   NULL,
    [changed_columns]    NVARCHAR (MAX)   NULL,
    PRIMARY KEY CLUSTERED ([id] ASC)
);

CREATE TABLE [dbo].[tbl_test_role] (
    [id]   INT           IDENTITY (1, 1) NOT NULL,
    [name] NVARCHAR (50) NOT NULL,
    [details] NVARCHAR (150) NULL,
    PRIMARY KEY CLUSTERED ([id] ASC)
);
  • [tbl_test_audit_trail] 将存储审核数据
  • [tbl_test_role] 简单/测试数据表

删除对象

DROP TABLE [dbo].[tbl_test_audit_trail]
DROP TABLE [dbo].[tbl_test_role]

使用DbContext

在这里,我们正在使用实体框架相关操作Insertupdatedelete。而不是调用默认的SaveChanges(),我们使用SaveChanges(string userName)创建审核日志。

IMopDbContext Db = new MopDb();
string user = "userName";

/*Insert*/
Role role = new Role()
{
    Name = "Role",
};
Db.Role.Add(role);
Db.SaveChanges(user);


/*Update detail column*/
role.Details = "Details";
Db.SaveChanges(user);
/*Update name column*/
role.Name = role.Name + "1";
Db.SaveChanges(user);
/*Update all columns*/
role.Name = "Role All";
role.Details = "Details All";
Db.SaveChanges(user);

/*Delete*/
Db.Role.Remove(role);
Db.SaveChanges(user);

让我们检查一下[tbl_test_audit_trail],审计日志表,审计日志将如下所示:

解决方案和项目

它是带有.NET Core 2.2项目的Visual Studio 2017解决方案:

  • Db 包含与数据库和实体框架相关的代码
  • Test.Integration 包含集成的NUnit单元测试

Test.Integration 项目内部,我们需要在appsettings.json处更改连接字符串:

"ConnectionStrings": {
  /*test*/
  "MopDbConnection": "server=10.10.20.18\\DB03;database=TESTDB;
                      user id=TEST;password=dhaka" /*sql server*/
},

参考文献

局限性

  • 避免使用DbContext.AutoDetectChangesEnabledfalseAsNoTracking()
  • 在使用此跟踪帮助器时,如果我们添加/更新/删除1行,它将添加/更新/删除2行。实体框架不适用于大型数据集。在处理了很多行(如100-200)之后,我们应该重新初始化DbContext对象。
  • 该审核跟踪器无法跟踪Db生成的值,例如IDENTITY。有可能,但是如果管理不当,可能会导致事务失败。查看该选项的审核历史记录文章。
  • 我们将存储类属性名称,而不是实际的列名称。
  • 性能可能是一个问题。

对于未经测试的输入,该代码可能会引发意外错误。如果有的话,请告诉我。

下一步是什么?

  • 支持Db生成的值
  • 为实体框架创建相同的东西
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值