使用實體框架核心創建簡單的審計跟蹤

目錄

介紹

背景

審覈日誌數據

審覈表實體

審覈類型

審計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生成的值
  • 爲實體框架創建相同的東西
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章