在領域驅動設計(DDD)中,有一個非常重要的概念:「強型別Id」。使用強型別Id來做標識屬性的型別會比用int、Guid等通用型別能帶來更多的好處。比如有一個根據根據Id刪除使用者的方法的簽名如下:
void RemoveById(long id);
我們從方法的引數看不出來id代表什麼含義,因此如果我們錯誤地把貨物的id傳遞給這個方法,那麼也是可以的。這樣用long等通用型別來表示標識屬性會讓引數等的業務屬性弱化。
而如果我們自定義一個UserId型別,如下:
class UserId { public long Value{get;init;} public UserId(long value) { this.Value=value; } }
這樣User類的定義中Id屬性的型別就從long變成了UserId型別,如下:
class User { public UserId Id{get;} public string Name{get;set;} }
對應的RemoveById方法的簽名也變成了:
void RemoveById(UserId id);
這樣不僅能一看就看出來id引數代表的業務含義,也能避免「把貨物Id的值傳遞給使用者Id引數」這樣的問題。
在.NET 6及之前,Entity Framework Core(簡稱EF Core)中很難優美地實現強型別Id。在.NET7中,EF Core中提供了對強型別Id的支援,具體用法請參考EF Core官方檔案中「Value generation for DDD guarded types」這部分內容。
儘管EF Core已經內建了對強型別Id的支援,但是它需要程式設計師編寫非常多的程式碼。比如一個比較完善的強型別Id類的程式碼就要編寫如下30多行程式碼:
public readonly struct PersonId { public Guid Value { get; } public PersonId(Guid value) { Value = value; } public override string ToString() { return Convert.ToString(Value); } public override int GetHashCode() { return Value.GetHashCode(); } public override bool Equals(object obj) { if (obj is PersonId) { PersonId objId = (PersonId)obj; return Value == objId.Value; } return base.Equals(obj); } public static bool operator ==(PersonId c1, PersonId c2) { return c1.Equals(c2); } public static bool operator !=(PersonId c1, PersonId c2) { return !c1.Equals(c2); } }
還要編寫一個ValueConverter類以及設定自定義的ValueGenerator……需要編寫的程式碼的複雜程度讓想使用強型別Id的開發者望而卻步。
正因為這一點,所以連微軟的檔案中都警告到"強型別Id會增加程式碼的複雜性,請謹慎使用"。幸好,這個世界有我!
為了解決這個問題,我基於.NET的SourceGenerator技術編寫了一個開源專案,這個開源專案會在編譯時自動生成相關的程式碼,開發人員只要在實體類上標註一個[HasStronglyTypedId]即可。
專案地址:https://github.com/yangzhongke/LessCode.EFCore.StronglyTypedId
下面我用一個把所有程式碼都寫到一個控制檯專案中的例子來演示它的用法,多專案分層等更復雜的用法請見專案檔案以及專案中的Examples資料夾中的內容。
注意:這個專案可能會隨著升級而用法有所變化,具體用法請以最新官方檔案為準。
用法:
1、 新建一個.NET7控制檯專案,然後依次安裝如下這些Nuget包:LessCode.EFCore、LessCode.EFCore.StronglyTypedIdCommons、LessCode.EFCore.StronglyTypedIdGenerator。當然我們的專案要使用SQLServer以及使用EF core的migration,所以還要安裝如下的Nuget包:Microsoft.EntityFrameworkCore.SqlServer、Microsoft.EntityFrameworkCore.Tools。
2、 專案中新建一個實體型別Person
[HasStronglyTypedId] class Person { public PersonId Id { get; set; } public string Name { get; set; } }
我們注意到Person上標註的[HasStronglyTypedId(typeof(Guid))],它代表這個類啟用強型別Id,編譯器在編譯的時候自動生成一個名字叫PersonId的類,所以我們就宣告了一個名字叫Id、型別為PersonId的屬性來表示實體的標識。
PersonId在資料庫中儲存的預設是long型別,如果想儲存為Guid型別,就可以寫成[HasStronglyTypedId(typeof(Guid))]。
編譯一下專案,如果能夠編譯成功,我們反編譯生成的dll,就能看到dll中自動生成了PersonId、PersonIdValueConverter兩個類。
3、 編寫DbContext,程式碼如下:
using LessCode.EFCore; class TestDbContext:DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(自己的連線字串); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ConfigureStronglyTypedId(); } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { base.ConfigureConventions(configurationBuilder); configurationBuilder.ConfigureStronglyTypedIdConventions(this); } }
4、 進行資料庫的遷移等操作,這部分屬於EF Core的標準操作,我不再介紹。對EF Core的用法不熟悉的朋友,請到嗶哩嗶哩、youtube等平臺搜尋「楊中科 .NET Core教學」。
5、 編寫程式碼進行測試
using TestDbContext ctx = new TestDbContext(); Person p1 = new Person(); p1.Name = "yzk"; ctx.Persons.Add(p1); ctx.SaveChanges(); PersonId pId1 = p1.Id; Console.WriteLine(pId1); Person? p2 = FindById(new PersonId(1)); Console.WriteLine(p2.Name); Person? FindById(PersonId pid) { using TestDbContext ctx = new TestDbContext(); return ctx.Persons.SingleOrDefault(p => p.Id == pid); }
強型別Id讓我們能夠更好的在EFCore中實現DDD,我開源的這個專案能夠讓開發者只要在實體類上標註一行[HasStronglyTypedId]就可以完成強型別Id的使用。希望它能夠幫到你,歡迎把它分享到你所在的技術社群。