持久化與模型設定
DomainKit 的 EF Core 套件提供 Repository、IUnitOfWork、聚合導覽載入與共用模型設定。Repository 負責載入及追蹤聚合,查詢投影則交由 QueryService,避免寫入模型與讀取模型混用。
Repository
自訂 Repository 繼承 Repository<TDbContext, TEntity, TKey>。TKey 代表應用程式用來尋找聚合的 Domain Key,不一定等於資料庫主鍵。
public sealed class CustomerRepository
: Repository<AppDbContext, Customer, string> {
public CustomerRepository(AppDbContext dbContext, IUnitOfWork unitOfWork)
: base(dbContext, unitOfWork) {
}
}
鍵值判定依下列順序執行:
- 實體實作
IHasExternalKey<TKey>時,使用ExternalKey。 - 其餘實體使用 EF Core model 定義的單一主鍵。
- 複合主鍵須由 Repository 覆寫
CreateKeyPredicate,預設實作不支援。
FindAsync 找不到資料時回傳 null;GetAsync、UpdateAsync 與 DeleteAsync 找不到資料時拋出 EntityNotFoundException。
CreateAsync、UpdateAsync 與 DeleteAsync 都回傳受影響的實體,UpdateAsync 與 DeleteAsync 回傳的是已被追蹤的執行個體,可直接接續讀取更新後的狀態。UpdateAsync 另有接受 Func<TEntity, CancellationToken, Task> 的多載,供更新過程需要 await 其他服務時使用。
延後提交
CreateAsync、UpdateAsync 與 DeleteAsync 的 autoCommit 預設為 false。多個聚合操作可累積在同一個 DbContext,最後由 IUnitOfWork 提交一次。
await customerRepository.CreateAsync(customer, cancellationToken: ct).ConfigureAwait(false);
await addressRepository.CreateAsync(address, cancellationToken: ct).ConfigureAwait(false);
await unitOfWork.SaveChangesAsync(actorId, ct).ConfigureAwait(false);
只有確定單一操作需要立即落地時才使用 autoCommit: true。此模式會在 Repository 方法內呼叫 SaveChangesAsync。Repository 方法的 actorId 參數會一併傳給該次提交,因此只在 autoCommit: true 時有作用;延後提交的情境由 IUnitOfWork.SaveChangesAsync 指定操作者。
DiscardChanges 會清除 Change Tracker,以及目前追蹤聚合尚未派發的兩類事件。它適合在應用層確定放棄整個工作單元時使用。
SaveChangesAsync 過程中若拋出 BusinessException(包含由 domain event handler 拋出的),會先自動執行一次 DiscardChanges 再重新拋出。業務規則擋下的提交不會留下半套的追蹤狀態,但同一個 DbContext 上尚未提交的其他變更也會一併失去,捕捉此例外後需重新載入聚合。
明確交易
UseTransactionAsync 在目前沒有交易時建立交易,成功時提交,失敗時回復。若 DbContext 已有交易,方法會重用既有交易。執行委派仍須自行呼叫 SaveChangesAsync。
await unitOfWork.UseTransactionAsync(
async cancellationToken => {
await customerRepository.CreateAsync(customer, cancellationToken: cancellationToken)
.ConfigureAwait(false);
await unitOfWork.SaveChangesAsync(actorId, cancellationToken).ConfigureAwait(false);
},
ct
).ConfigureAwait(false);
需要在交易範圍內取得回傳值,或需要直接操作交易物件時,改用泛型多載。委派會收到目前的 IDbContextTransaction,可用於建立 savepoint 或把交易交給其他需要它的 API。
Guid customerId = await unitOfWork.UseTransactionAsync(
async (transaction, cancellationToken) => {
await customerRepository.CreateAsync(customer, cancellationToken: cancellationToken)
.ConfigureAwait(false);
await unitOfWork.SaveChangesAsync(actorId, cancellationToken).ConfigureAwait(false);
return customer.Id;
},
ct
).ConfigureAwait(false);
重用既有交易時,方法不會提交也不會回復該交易,交由原本開啟交易的那一層負責。
軟刪除判定
DeleteAsync 依實體能力決定刪除方式:
- 實作
IHasDeletedTime的實體會寫入DeletedTime,並保持資料列存在。 - 未實作
IHasDeletedTime的實體會由DbSet.Remove實體刪除。
完整審計實體應搭配 ConfigureFullAuditedEntity。該設定會加入 DeletedTime is null 的全域查詢篩選。詳細行為見聚合與審計。
聚合導覽載入
Repository 可從 EF Core model annotation 取得聚合邊界內必須載入的導覽路徑。使用 HasAggregateIncludes 集中宣告,避免每個 Repository 重複撰寫 Include。
modelBuilder.Entity<SalesOrder>(builder => {
builder.HasAggregateIncludes(includes => includes
.Include(order => order.Lines, lines => lines
.Include(line => line.Adjustments))
.Include(order => order.BillingAddress));
});
FindAsync、GetAsync、UpdateAsync 與 DeleteAsync 都會套用這些路徑。Repository 仍可覆寫 IncludeQuery 加入無法由共用設定表達的查詢內容。
聚合 Include 應只涵蓋維護聚合一致性所需的導覽。清單與報表查詢使用投影,不應為了顯示欄位而擴張聚合載入範圍。
全域型別轉換
ApplyDomainKitConventions 提供 DateTimeOffset converter 的宿主接點。DomainKit 不依資料庫 provider 自動選擇儲存型別。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) {
base.ConfigureConventions(configurationBuilder);
configurationBuilder.ApplyDomainKitConventions(new UtcDateTimeOffsetConverter());
}
UtcDateTimeOffsetConverter 將值正規化為 offset 0 並維持 DateTimeOffset 儲存型別。UtcDateTimeConverter 將 DateTimeOffset 轉成 UTC DateTime,適用於需要以 DateTime 儲存的 provider。未傳入 converter 時,DomainKit 不修改 EF Core 的預設對映。
Enumeration 對映
採用 CloudyWing.Enumeration 的專案可在 ConfigureConventions 掃描包含 Enumeration 型別的組件。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) {
base.ConfigureConventions(configurationBuilder);
configurationBuilder.ApplyDomainKitConventions(new UtcDateTimeOffsetConverter());
configurationBuilder.ApplyEnumerationConventions(typeof(OrderStatus).Assembly);
}
每個具體 EnumerationBase<TEnum, TValue> 型別會使用其 Value 型別儲存。顯示名稱本地化見錯誤契約與本地化。