使用 Entity Framework Core 创建简单的审计跟踪






4.39/5 (6投票s)
将所有数据更改历史记录存储在表中使用 Entity Framework Core
引言
在一个特定的项目中,我们需要记录任何最终用户所做的数据更改。这需要在不大量修改现有解决方案代码的情况下完成。该项目正在使用 Entity Framework,所以我认为为什么不直接在 SaveChanges()
方法中实现呢。
背景
数据库是 SQL Server,ORM 是 Entity Framework 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
: 删除现有行
审计数据库上下文
创建一个接口来指定基于审计日志的数据库上下文,用于 Entity Framework
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; /*For EF Core 7: Entry.Metadata.GetTableName();*/
foreach (PropertyEntry property in Entry.Properties)
{
string propertyName = property.Metadata.Name;
string dbColumnName = property.Metadata.Relational().ColumnName; /*For EF Core 7: property.Metadata.GetColumnName();*/
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
的接口 IMopDbContext
,以创建 DbContext
对象。
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
实体,考虑所有实体更改。然后,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;
}
}
}
数据库:SQL Server
为进行测试,我们使用了 MS SQL Server 数据库,但它适用于任何数据库。
查找相关表对象的脚本如下。
创建对象
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
在这里,我们使用 Entity Framework 执行 Insert
、update
和 delete
相关操作。我们不是调用默认的 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]
,审计日志表,审计日志将是这样的
解决方案和项目
这是一个 **Visual Studio 2017** 解决方案,包含 **.NET Core 2.2** 项目
Db
包含数据库和 Entity Framework 相关代码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*/
},
参考文献
- 如何使用 Entity framework 5 和 MVC 4 创建审计日志
- 使用 Entity Framework 实现审计日志:第一部分
- 我最喜欢的,感谢 @meziantou, Entity Framework Core:历史/审计表
限制
- 避免使用
DbContext.AutoDetectChangesEnabled
=false
或AsNoTracking()
- 在使用此日志辅助程序时,如果我们添加/更新/删除 1 行,它会添加/更新/删除 2 行。Entity Framework 对于大量数据集处理不是很好。我们应该在处理完一定数量的行(例如 100-200 行)后重新初始化
DbContext
对象。 - 此审计日志程序无法跟踪数据库生成的 `IDENTITY` 等值。这是可能的,但如果管理不当,可能会导致事务失败。请查看此 审计历史记录 文章了解该选项。
- 我们存储的是类属性名,而不是实际的列名。
- 性能可能是一个问题。
该代码对于未经测试的输入可能会抛出意外错误。如果有,请告诉我。
下一步?
- 支持数据库生成的数值
- 为 Entity Framework 创建同样的东西
历史
- 2020年4月11日:初始版本