在领域驱动设计(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的使用。希望它能够帮到你,欢迎把它分享到你所在的技术社区。

我开发的开源项目,让.NET7中的EFCore更轻松地使用强类型Id的更多相关文章

  1. 使用GitHub进行协同项目开发和开源项目贡献

    本教程致力于摆脱git命令行快速的学习使用GitHub. 此次是GitHub课程的第三次课程,也是最后一次课程.推荐进行按照次序查看本次教程.上篇文章:程序员,一起玩转GitHub版本控制,超简单入门 ...

  2. 《云阅》一个仿网易云音乐UI,使用Gank.Io及豆瓣Api开发的开源项目

    CloudReader 一款基于网易云音乐UI,使用GankIo及豆瓣api开发的符合Google Material Desgin阅读类的开源项目.项目采取的是Retrofit + RxJava + ...

  3. iOS开发之开源项目链接

    1. Coding iOS 客户端 Coding官方客户端. 笔者强烈推荐的值得学习的完整APP.GitHub - Coding/Coding-iOS: Coding iOS 客户端源代码 2. OS ...

  4. 流媒体开发之开源项目live555---live555 server 编译 包括更改帧率大小

    由于要测试8148解码器的性能,需要搭建不同帧率25fps - >30fps,宏块大小defualt 100 000 -> 200 000不同大小的h264码流,所以就需要编译改动的liv ...

  5. 流媒体开发之开源项目live555---更改server端的帧率大小和码率大小

    -----------------------------qq:1327706646 010101010101010110010101010101010101010author:midu 010101 ...

  6. 一款属于自己的笔记本【vue+gin+elementUI前后端分离开发部署开源项目】

    前言 我为什么要写一个个人的云笔记? (⊙o⊙)-额额额

  7. 一劳永逸:域名支持通配符,ASP.NET Core中配置CORS更轻松

    ASP.NET Core 内置了对 CORS 的支持,使用很简单,只需先在 Startup 的 ConfigureServices() 中添加 CORS 策略: public void Configu ...

  8. .NET平台开源项目速览(4).NET文档生成工具ADB及使用

    很久以前就使用ADB这个工具来生成项目的帮助文档.功能强大,在学习一些开源项目的过程中,官方没有提供CHM帮助文档,所以为了快速的了解项目结构和注释.就生成文档来自己看,非常好用.这也是一个学习方法吧 ...

  9. 直接拿来用!最火的Android开源项目(完结篇)

    直接拿来用!最火的Android开源项目(完结篇) 2014-01-06 19:59 4785人阅读 评论(1) 收藏 举报 分类: android 高手进阶教程(100) 摘要:截至目前,在GitH ...

  10. 直接拿来用!最火的Android开源项目(完结篇)(转)

    摘要:截至目前,在GitHub“最受欢迎的开源项目”系列文章中我们已介绍了40个Android开源项目,对于如此众多的项目,你是Mark.和码友分享经验还是慨叹“活到老要学到老”?今天我们将继续介绍另 ...

随机推荐

  1. [机器学习]-分类问题常用评价指标、混淆矩阵及ROC曲线绘制方法

    分类问题 分类问题是人工智能领域中最常见的一类问题之一,掌握合适的评价指标,对模型进行恰当的评价,是至关重要的. 同样地,分割问题是像素级别的分类,除了mAcc.mIoU之外,也可以采用分类问题的一些 ...

  2. ESX添加过时的硬件

    昨天遇到了个ESX上有旧的qlogic的FC卡无法识别的问题.无法识别也就无法连接存储,整个ESX就基本无法使用了.所以,需要检查硬件.一开始,在查了很多资料各种命令检查,走了很多弯路.这里我总结一下 ...

  3. 利用rpmbuild 打包可执行文件和链接库生成rpm 包

    Background: Background: 遇到一个打包可执行程序和链接库生成rpm 包的需求,查遍了很多网站的资料,现在整理下解决方案. 前期参考的是这篇帖子: https://codeante ...

  4. 请求体: Request Body

    官方文档地址:https://fastapi.tiangolo.com/zh/tutorial/body/ # -*- coding: UTF-8 -*- from fastapi import Fa ...

  5. Elasticsearch:Pinyin 分词器

    Elastic的Medcl提供了一种搜索Pinyin搜索的方法.拼音搜索在很多的应用场景中都有被用到.比如在百度搜索中,我们使用拼音就可以出现汉字: 对于我们中国人来说,拼音搜索也是非常直接的.那么在 ...

  6. 第二章:视图层 - 9:动态生成CSV文件

    CSV (Comma Separated Values),以纯文本形式存储数字和文本数据的存储方式.纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样的数据.CSV文件由任意数目的记录组成,记 ...

  7. 不可错过的效能利器「GitHub 热点速览 v.22.39」

    如果你是一名前端工程师且维护着多个网站,不妨试试本周榜上有名的 HTML-first 的 Qwik,提升网站访问速度只用一招.除了提升网站加载速度的 Qwik,本周周榜上榜的 Whisper 也是一个 ...

  8. 通过js实现随机生成图片

    这次给大家分享一个通过js向HTML添加便签,实现随机代码生成的案例,代码已经放在下方,这里我在下面准备了50张图片,但是没有放在博文中,如果读者想要练习,可以自己下载一些图片,建议下载的多一些. & ...

  9. 引擎之旅 Chapter.4 日志系统

    关于近段时间为何没有更新的解释:Find a new job. 目录 引言 日志语句的分类 控制台窗体 和 VSOutput Tab的日志打印 存储至特定的文件中 展示堆栈信息 引言 一般来说,一个优 ...

  10. day46-JDBC和连接池02

    JDBC和连接池02 3.ResultSet[结果集] 基本介绍 表示数据库结果集的数据表,通常通过执行查询数据库的语句生成 ResultSet对象保持一个光标指向其当前的数据行,最初,光标位于第一行 ...