目录
介绍
背景
审核日志数据
审核表实体
审核类型
审计Db上下文
审核表配置
数据更改到审核表
实体更改为审核表实体
所有实体更改为审计表
在现有的DbContext中使用审核跟踪
创建DbContext
db:SQL服务器
创建对象
删除对象
使用DbContext
解决方案和项目
参考文献
局限性
下一步是什么?
- 下载源29.2 KB
在一个特定的项目中,我们必须记录任何最终用户所做的数据更改。这需要在不对现有解决方案进行很多代码更改的情况下完成。该项目使用的是Entity Framework,所以我想为什么不在SaveChanges()方法内部完成。
背景该数据库是SQL Server,ORM实体框架核心(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:现有行已删除
创建一个接口以为实体框架指定基于审计跟踪的数据库上下文:
using Db.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace Db
{
public interface IAuditDbContext
{
DbSet Audit { get; set; }
ChangeTracker ChangeTracker { get; }
}
}
- DbSet 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
{
public void Configure(EntityTypeBuilder 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
KeyValues { get; } = new Dictionary();
public Dictionary
OldValues { get; } = new Dictionary();
public Dictionary
NewValues { get; } = new Dictionary();
public List ChangedColumns { get; } = new List();
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 auditEntries = new List();
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 { get; set; }
DatabaseFacade Database { get; }
int SaveChanges(string userName);
}
}
- DbSet Role { get; set; } 是现有的数据表。
- 在SaveChanges(string userName内部),我们将使用AuditHelper类来考虑所有实体更改来创建Audit实体。然后,审核实体将被添加到审核跟踪表中。
在现有/测试数据库环境中,我们将要:
- 添加审核表DbSet 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 { get; set; }
public virtual DbSet Role { get; set; }
public MopDbContext(DbContextOptions 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;
}
}
}
db:SQL服务器
为了进行测试,我们使用的是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
在这里,我们正在使用实体框架相关操作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],审计日志表,审计日志将如下所示:
它是带有.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*/
},
参考文献
- 如何使用Entity Framework 5和MVC 4创建审核跟踪
- 使用实体框架实施审核跟踪:第1部分
- 我最喜欢的,感谢@meziantou,实体框架核心:历史/审计表
- 避免使用DbContext.AutoDetectChangesEnabled= false或AsNoTracking()
- 在使用此跟踪帮助器时,如果我们添加/更新/删除1行,它将添加/更新/删除2行。实体框架不适用于大型数据集。在处理了很多行(如100-200)之后,我们应该重新初始化DbContext对象。
- 该审核跟踪器无法跟踪Db生成的值,例如IDENTITY。有可能,但是如果管理不当,可能会导致事务失败。查看该选项的“审核历史记录”文章。
- 我们将存储类属性名称,而不是实际的列名称。
- 性能可能是一个问题。
对于未经测试的输入,该代码可能会引发意外错误。如果有的话,请告诉我。
下一步是什么?- 支持Db生成的值
- 为实体框架创建相同的东西