本文主要介紹 DDD 中的強類型 ID 的概念,及其在 EF 7 中的實現,以及使用 LessCode.EFCore.StronglyTypedId 這種更簡易的上手方式。
背景
在楊中科老師 B 站的.Net Core 視頻教程[1]其中 DDD 部分講到了強類型 ID(Strongly-typed-id)的概念,也叫受保護的密鑰(guarded keys)當時在 .NET 中的 DDD 實現是個懸而未決的問題,之后我也一直在尋找相關的實現方案。
非常高興 .NET 7 的更新帶來的 EF Core 7.0 的新增功能中,就包含了改進的值生成[2]這一部分,在自動生成關鍵屬性的值方面進行了兩項重大改進。
下面我們通過幾個例子來了解這部分的內容,以及如何更簡便的實現強類型。
強類型 ID
強類型 ID(Strongly-typed-id),又稱之為受保護的鍵(guarded keys),它是領域驅動設計(DDD) 中的一項不可或缺的功能。
簡單的來說,就是比如兩個實體都是 int、long 或是 Guid 等類型的鍵值 ID,那么這就意味著它們 ID 就有可能在編碼時被我們分配錯誤。再者一個函數如果同時傳這兩個 ID 作為參數,順序傳入錯誤,就意味著執行的結果出現問題。
在 DDD 的概念中,可以將實體的 ID 包裝到另一種特定的類型中來避免。比如將 User 的 int 型 Id 包裝為 UserId 類型,只用來它來表示 User 實體的 Id:
// 包裝前
public class User
{public int Id { get; set; }
}// 以下是包裝后
public class User
{public UserId Id { get; set; }
}
其優點非常明顯:
?代碼自解釋,不需要多余的注釋就可以看明白,提高程序的可讀性?利用編譯器提前避免不經意的編碼錯誤,提高程序的安全性
當然上面的代碼并不是具體實現的全部,需要其他更多的額外編碼工作。也就是說其增加了代碼的復雜性。DDD 中更多的是規范性設計,是為了預防缺陷的發生,讓代碼也變的更易懂了。具體是否要使用某一條規范,我們可以根據項目的具體情況進行權衡。
缺陷也總會有解決方案,集體的智慧是無窮,已經有很多技術大牛提供了更簡便的方案,我們只需要站在巨人的肩膀上體驗強類型 ID 帶來的優點和便捷就可以了,文章也會介紹如何更簡易的實現。
EF 中的使用演示
我們首次創建一個未使用強類型 ID 的 Demo,之后用不同方法實現強類型 ID 進行比較。項目都選擇 .NET 7,數據庫這里使用的是 MySql 。MySQL 中對 EF Core 7.0 的支持需要用到組件?Pomelo.EntityFrameworkCore.MySql
?,當前需要其 alpha 版本。
1. 未使用強類型 ID
創建一個用于生成作者表的?Author
?實體:
internal class Author
{public long Id { get; set; }public string Name { get; set; }public string Description { get; set; }
}
接下來創建一個用于生成圖書表的?Book
?實體:
internal class Book
{public Guid Id { get; set; }public string BookName { get; set; }public Author? Author { get; set; }public long AuthorId { get; set; }
}
然后創建對應的?DbContext
:
internal class TestDbContext : DbContext
{public DbSet<Book> Books { get; set; }public DbSet<Author> Authors { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){string connStr = "Server=localhost;database=test;uid=root;pwd=root;";var serverVersion = new MySqlServerVersion(new Version(8, 0, 27));optionsBuilder.UseMySql(connStr, serverVersion);optionsBuilder.LogTo(Console.WriteLine);}}
進行數據庫遷移,我們可以發現其創建的數據庫表情況如下:

然后在?Program.cs
?中編寫下列測試添加和查詢的代碼:
using ordinary;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;TestDbContext ctx = new TestDbContext();var zack = new Author
{Name = "zack",Description = "mvp"
};ctx.Authors.Add(zack);ctx.SaveChanges();ctx.Books.Add(new Book {Author= zack,BookName = "ddd .net",
});ctx.SaveChanges();var list1 = ctx.Authors.ToArray();
var list2 = ctx.Books.ToArray();Console.WriteLine("\n\n--------------------- Author Table Info -------------------------");Console.WriteLine(JsonSerializer.Serialize(list1));Console.WriteLine("\n\n--------------------- Book Table Info -------------------------");Console.WriteLine(JsonSerializer.Serialize(list2));
其執行結果如下:

2. 基礎實現
接下來我們按照官網的說明對以上的代碼進行改造,實現基本的強類型 ID。
我們按照說明先定義類型,對兩個類進行改造。
internal class Book
{public BookId Id { get; set; }public string BookName { get; set; }public Author? Author { get; set; }public AuthorId AuthorId { get; set; }
}public readonly struct BookId
{public BookId(Guid value) => Value = value;public Guid Value { get; }
}
internal class Author
{public AuthorId Id { get; set; }public string Name { get; set; }public string Description { get; set; }}public readonly struct AuthorId
{public AuthorId(long value) => Value = value;public long Value { get; }
}
此時直接遷移肯定是會報錯的:
The property 'Author.Id' could not be mapped because it is of type 'AuthorId', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

強類型 ID 在數據庫里面的表示還是原始的類型,我們還需要在?DbContext
?中通過為類型定義值轉換器來實現轉換:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{configurationBuilder.Properties<AuthorId>().HaveConversion<AuthorIdConverter>();configurationBuilder.Properties<BookId>().HaveConversion<BookIdConverter>();
}private class AuthorIdConverter : ValueConverter<AuthorId, long>
{public AuthorIdConverter(): base(v => v.Value, v => new(v)){}
}private class BookIdConverter : ValueConverter<BookId, Guid>
{public BookIdConverter(): base(v => v.Value, v => new(v)){}
}
接著還沒結束,我們還需要?DbContext.OnModelCreating
?中配置值轉換的,否則遷移后你會發現 Author 的主鍵自增沒有了,運行后的數據庫 Guid 還全變成 0 了。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{modelBuilder.Entity<Author>().Property(author => author.Id).ValueGeneratedOnAdd();modelBuilder.Entity<Book>().Property(book => book.Id).ValueGeneratedOnAdd();
}
3. 使用 LessCode.EFCore.StronglyTypedId 簡化
通過上一小節我們看到,雖然支持了強類型 ID ,但是要實現起來需要自行配置的東西還是非常多得,用的越多,額外代碼的工作量也隨之增長。雖然是在自己代碼里 Ctrl CV 但是多執行幾次也說不定會一個疏忽而出錯。
因為在 GitHub Follow 了楊中科老師,所以在幾天前發現了我們這位寶藏大男孩提供的新工具?LessCode.EFCore.StronglyTypedId
,開源地址:https://github.com/yangzhongke/LessCode.EFCore.StronglyTypedId,這個項目基于 source generator 技術,可以幫你生成額外的代碼,四舍五入約等于楊老師幫你把多余的代碼寫了。
根據說明文檔開始新的改造,首先安裝說需要的 Nuget 包,因為演示的 Demo 沒有分層,是一把梭哈的,直接安裝全部的包就可以了。分層的項目可以前往倉庫查看分層的使用文檔即可。
Install-Package LessCode.EFCore
Install-Package LessCode.EFCore.StronglyTypedIdGenerator
在改造上,只需要通過標識聲明這個類存在一個強類型 ID 即可,默認標識類型是 long ,對于?Author
?類,只需要直接添加?[HasStronglyTypedId]
?即可:
[HasStronglyTypedId]
internal class Author
{public AuthorId Id { get; set; }public string Name { get; set; }public string Description { get; set; }
}
對?Book
?類使用的 Guid 類型 ID,可以使用 HasStronglyTypedId 的構造函數來制定標識類型:
[HasStronglyTypedId(typeof(Guid))]
internal class Book
{public BookId Id { get; set; }public string BookName { get; set; }public Author? Author { get; set; }public AuthorId AuthorId { get; set; }
}
對于 DbContext 的修改,只需要做簡單的配置即可,無需根據強類型 ID 的使用情況自行進行繁雜的轉換和配置,這些將由?LessCode.EFCore
?根據?[HasStronglyTypedId]
?的標識進行處理。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{base.OnModelCreating(modelBuilder);modelBuilder.ConfigureStronglyTypedId();
}protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{base.ConfigureConventions(configurationBuilder);configurationBuilder.ConfigureStronglyTypedIdConventions(this);
}
如此這般,可謂簡便了不少。俗話說的好(我說的):輪子用的好,程序下班早。趕快去試起來吧!
最后
更多 LessCode.EFCore.StronglyTypedId 的介紹可前往: https://github.com/yangzhongke/LessCode.EFCore.StronglyTypedId。
文章相關 Demo 地址:https://github.com/sangyuxiaowu/StronglyTypedId
References
[1]
?.Net Core 視頻教程:?https://www.bilibili.com/video/BV1pK41137He/[2]
?改進的值生成:?https://learn.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-7.0/whatsnew#improved-value-generation