.Net Core6 實作 Entity FrameWork Core 6.0 於(新增/修改/刪除)時自動寫入Log表擴充功能
前言
Entity Framework (EF) Core 是常見 Entity Framework 資料存取技術的輕量型、可擴充、開放原始碼且跨平台版本,EF Core 可作為物件關聯對應 (ORM) 框架,如:
- 可讓 .NET 開發人員使用 .NET 物件來處理資料庫。
- 無須使用在一般情況下需要撰寫的大部分資料存取碼。
前置作業先建立好資料庫與資料表,如資料表需要追蹤異動LOG紀錄則需要以{資料表名}Log,本擴充會建立blob與blobLog兩張資料表,開發人員可以透過Scaffold-Database 指令將現有資料庫產生模型(DB-First)後,可透過EF對資料庫內的(資料表)進行查詢、儲存、修改、刪除動作。
以專案需求為例,希望使用者於系統內修改會員資料後需要留下異動紀錄,並可從系統內查詢,雖然此需求於現有EF架構內可以做到如:
db.Blog.Add(new Blog { Name = "EFlog",CreateTime=DateTime.Now });
db.Blog.SaveChanges();
db.BlogLog.Add(new Blog { Name = "EFlog",CreateTime=DateTime.Now,ModifyType="I",ModifyTime=DateTime.Now });
db.BlogLog.SaveChanges();
藉由擴充只要即可
db.Blog.Add(new Blog { Name = "EFlog",CreateTime=DateTime.Now });
db.Blog.SaveChanges();
擴充實作
進行擴充之需要進行前置作業,首先開發人員需進行資料庫產生模型(DB-First)於專案內產生EF Model 及 DbContext 類別的衍生程式,作為連結資料庫的入口。
範例於 Microsoft Visual Studio 2022 >套件管理器主控台輸入指令:
Scaffold-DbContext -Verbose "data source={DB Location};initial catalog={DB Name};persist security info=True;user id={帳號};password={密碼};encrypt=true;trustServerCertificate=true;MultipleActiveResultSets=True;App=EntityFramework" Microsoft.EntityFrameworkCore.SqlServer -DataAnnotations -NoOnConfiguring -Force -Project {DbContext產生命名空間位置} -OutputDir Models -Context SigvnpmyContext -ContextDir "Contexts" -NoPluralize
※此指令會將產生SigvnpmyContext並且繼承DbContext 類別的衍生程式。
※Scaffold-DbContext相關參數說明,如末端章節參考資料。
產生SigvnpmyContext類別後將自行在建立一個partial 類別(父類別),大家可能會有疑惑,為什麼需要建立partial 類別,因為系統在開發或維運時免不了異動到DB架構,比方說欄位增減或新增資料表等異動,此時開發專案就需要再重新一次DB First 確保EF Model 與DB一致,當在執行這個動作時如果我們把將要擴充的程式碼撰寫於類別時,就會被移除導致接下來要做的事情都功虧一簣了,如此一來使用 partial class就可以解決防止擴充程式碼被移除。
public partial class SigvnpmyContext
{
}
做好前置作業後即將開始實作,此範例的擴充時機點設定在SaveChangesAsync()
對此method 進行override,在回傳資料異動前新增擴充GetLogInstances()方法,此方法主要來追蹤Entity資料所做的任何變更,藉此用來判斷使用者是否進行Add/Update/Delete之動作,並取得異動資料表名稱查詢專案內是否有{資料表名}Log物件,若存在代表DB內的資料表已存在,並依照使用者異動的動作將資料寫入Log表。
完整擴充程式碼
/// <summary>
/// Partial class of SigvnpmyContext.
/// </summary>
public partial class SigvnpmyContext
{
private readonly List<EntityState> _needLogState
= new List<EntityState>() { EntityState.Added, EntityState.Modified, EntityState.Deleted };
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<SigvnpmyContext> _logger;
/// <summary>
/// 初始化 SigvnpmyContext 類別的實例(DI)
/// </summary>
/// <param name="options">DbContextOptions</param>
/// <param name="loggerFactory">ILoggerFactory</param>
public SigvnpmyContext(DbContextOptions<SigvnpmyContext> options, ILoggerFactory loggerFactory)
: base(options)
{
_loggerFactory = loggerFactory;
_logger = _loggerFactory.CreateLogger<SigvnpmyContext>();
}
/// <summary>
/// 覆寫 SaveChanges 方法
/// </summary>
/// <returns>異動的資料筆數</returns>
public override int SaveChanges()
{
this.AddRange(GetLogInstances());
return base.SaveChanges();
}
/// <summary>
/// 覆寫 SaveChangesAsync 方法
/// </summary>
/// <param name="cancellationToken">CancellationToken</param>
/// <returns>異動的資料筆數</returns>
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
this.AddRange(GetLogInstances());
return base.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// 取得對應的 log entity instances。
/// 判斷方式為:若 entry 的狀態為 Added、Modified、Deleted,檢查是否有對應的 log table,若有則建立 instance。
/// </summary>
/// <returns>已實例化但尚未加入 ChangeTraker 的 log entity instances</returns>
private List<object> GetLogInstances()
{
ChangeTracker.DetectChanges();
var sourceEntries = ChangeTracker.Entries().Where(e => _needLogState.Contains(e.State));
var logInstances = new List<object>();
foreach (var sourceEntry in sourceEntries)
{
// check if need log
var logTypeName = sourceEntry.Entity.GetType().FullName + "Log";
Type? logType = Type.GetType(logTypeName);
if (logType != null)
{
// create log object instance
object logInstance = Activator.CreateInstance(logType) ?? throw new InvalidOperationException();
// get entry for log object
var logEntry = this.Entry(logInstance);
// copy value from loged entry to log entry
logEntry.CurrentValues.SetValues(sourceEntry.Entity);
logEntry.Property("ModifyType").CurrentValue = GetState(sourceEntry.State);
logEntry.Property("ModifyTime").CurrentValue = DateTime.UtcNow;
logInstances.Add(logInstance);
}
}
return logInstances;
}
private string GetState(EntityState state)
{
return state switch
{
EntityState.Added => "I",
EntityState.Modified => "U",
EntityState.Deleted => "D",
_ => ""
};
}
}
實際結果
結論
透過複寫EF產生的類別及EF Core 內的ChangeTracker方法來追蹤Entity資料所做的任何變更來達到偵測是否進行Add/Update/Delete動作,使用system.getType()方法判斷資料表是否存在LOG表與是否需資料表需要新增LOG紀錄後,便可大大縮短程式碼行數及開發人員遺漏撰寫之風險。
補充說明:資料為新增狀態時若有使用到DB自動生成 IDENTITY值的欄位,須於Add前使用sequence取號並賦予該欄位值,這樣Log紀錄新增才會有該欄位值,否則該筆新增紀錄的IDENTITY欄位會是NULL。
參考資料