0%

[DotnetCore]ORM系列-EFCore:資料表共同欄位設定

前情提要

筆者工作環境中開發ASP.NET CORE Web API,資料庫存取部份,會交叉使用EF Core以及Dapper,筆者單位共同認知的切分方式為若單個資料表的CRUD操作,就使用EF Core,若複雜的查詢,或者呼叫Store Procedure等會使用Dapper完成操作。單個資料表的CRUD,基本上透過EF Core可以說是輕而易舉,會遇到一個問題是資料表共同欄位,類似於CreatedDate, CreatedBy, UpdatedDate, UpdatedBy這種紀錄變更者及變更時間部份,若寫在每一個Service中會顯得有點多餘,且沒有效率,擴充性也不好,假設現在要改變CreatedDate, UpdatedDate的時間原本由Local Time改為UTC Time,其不就是要在每個Service中做修正。

內容

鑒於此,應該要想個辦法找出共同方法中做統一處理,筆者知道EF CoreSaveChanges()方法是可以覆寫的,也就是說我們只要在SaveChanges()方法中判斷新增或更新,針對不同欄位做更新值的動作,等於完成幫所有資料表的共同欄位做更新。

筆者工作環境中遇到的情境通常都是資料庫端已設計好資料結構,再撰寫程式的居多,這樣一來EF Core部份會選擇使用scaffold的方式自動產生資料庫對應的物件類別。筆者會在另一篇獨立講解scaffold的使用方式,這篇就先跳過,再來因資料表對應物件類別為工具產生出來的,盡量不要手動改寫,這時候要托Partial Class之福,我們可以設計Partial Class做覆寫作業。

設計共同欄位的介面

1
2
3
4
5
6
7
public interface IChangesEntity
{
DateTimeOffset CreatedDate { get; set; }
long CreatedBy { get; set; }
DateTimeOffset? UpdatedDate { get; set; }
long? UpdatedBy { get; set; }
}

製作共同欄位介面

主要是將把共同欄位做介面的宣告,這個是一個通用的技巧,若宣告介面,你寫泛型T相關Method或者類別都有幫助,因為那個泛型T限制為要實作某一個介面,好處就是在程式碼中可以點出相關Property。我們就沿用這個概念於此,就把共同欄位抽出來變成一個獨立的介面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 共同欄位-主要識別碼
public interface IEntity
{
long Id { get; set; }
}
// 實作軟刪除
public interface IDeleteEntity
{
bool IsDelete { get; set; }
}
// 共同欄位-新增編輯會用到
public interface IChangesEntity
{
DateTimeOffset CreatedDate { get; set; }
long CreatedBy { get; set; }
DateTimeOffset? UpdatedDate { get; set; }
long? UpdatedBy { get; set; }
}

宣告各類別的Partial Class

基本上Entities類別是由scaffold自動產出,我們為了各類別有實作上步驟所制定的介面,我們必須自己宣告一個Partial Class,實質意義只是為了宣告實作介面而已。

1
2
3
4
5
6
7
// 這邊namespace要特別注意,須與相對應的實際Entity類別同一個namespace,才有Partial效果
namespace XXX.Entities
{
public partial class Account : IEntity, IChangesEntity
{
}
}

DBContext覆寫SaveChanges()

基本上API部份有做Authorize,托.net core DI設計之福,很容易取得登入者,只要注入IHttpContextAccessor,可以輕鬆取得該操作者(登入者)資訊。再來就是依照Entry的狀態,可以針對相對應的欄位做編輯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public partial class XXContext
{
private readonly long _userId;

public XXContext(DbContextOptions<XXContext> options
, IHttpContextAccessor context) : base(options)
{
var userId = context?.HttpContext?.User?.FindFirst("id");
_userId = userId != null ? Convert.ToInt64(userId.Value) : 0;
}

public override int SaveChanges()
{
ChangeTracker.DetectChanges();
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is IChangesEntity ec)
{
switch (entry.State)
{
case EntityState.Added:
ec.CreatedDate = DateTimeOffset.UtcNow;
ec.CreatedBy = _userId;
ec.UpdatedDate = DateTimeOffset.UtcNow;
ec.UpdatedBy = _userId;
break;
case EntityState.Modified:
ec.UpdatedDate = DateTimeOffset.UtcNow;
ec.UpdatedBy = _userId;
break;
case EntityState.Detached:
break;
case EntityState.Unchanged:
break;
case EntityState.Deleted:
break;
default:
throw new ArgumentOutOfRangeException();
}
}

if (entry.Entity is IDeleteEntity e)
{
switch (entry.State)
{
case EntityState.Added:
e.IsDelete = false;
break;
case EntityState.Deleted:
e.IsDelete = true;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
return base.SaveChanges();
}

}

宣告完這些步驟後,使用端完全沒有感覺到任何的改變,只要操作DbContext下相對應的Entity物件,只要做SaveChanges(),就會執行base的SaveChanges()之前把那些共同欄位值做更新作業,達成擴充性高的一種實作方式。

結論

這篇主要利用Partial ClassOverride的技巧,達到其統一由一套Method將共同欄位值更新作業,擴充性高,筆者也在上面文中舉過例子,編輯時間的取值調動,我們可以再想一個情境,編輯者目前是透過IHttpContextAccessor注入的方式取得,若要改成其他方式取得編輯者,若設定編輯者的程式碼散落在各個地方是,不可能確保沒有漏網之魚,且花費成本顯然比改DbContext.Partial.cs來得高很多,因此筆者認為此文中的解法已經算上上之選了。

參考