我開發的開源項目,讓.NET7中的EFCore更輕鬆地使用強類型Id

在領域驅動設計(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]即可。

項目地址://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的使用。希望它能夠幫到你,歡迎把它分享到你所在的技術社區。

Tags: