目錄
介紹
在一個特定的項目中,我們必須記錄任何最終用戶所做的數據更改。這需要在不對現有解決方案進行很多代碼更改的情況下完成。該項目使用的是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:現有行已刪除
審計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;
}
}
}
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生成的值
- 爲實體框架創建相同的東西