Log EF Core

.Net Core6 實作 Entity FrameWork Core 6.0 於(新增/修改/刪除)時自動寫入Log表擴充功能

【Jimmy Lin】林昱成 (昕力 DTD) / 林昱成 Jimmy Lin 2023/03/30 14:50:26
4076

前言

Entity Framework (EF) Core 是常見 Entity Framework 資料存取技術的輕量型、可擴充、開放原始碼且跨平台版本,EF Core 可作為物件關聯對應 (ORM) 框架,如:

  1. 可讓 .NET 開發人員使用 .NET 物件來處理資料庫。
  2. 無須使用在一般情況下需要撰寫的大部分資料存取碼。

前置作業先建立好資料庫與資料表,如資料表需要追蹤異動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。

 

參考資料

 

  1. 開始使用 EF Core
  2. Scaffold-DbContext相關參數說明
  3. 使用 Entity Framework 6 儲存資料

 

【Jimmy Lin】林昱成 (昕力 DTD)