在领域驱动设计(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. 批量修改DNS记录的TTL值

    最近有个需求,需要修改Windows DNS服务器上区域下所有A记录的TTL值.原先默认的TTL是1小时.也就是说,其它DNS服务器会缓存查询到的记录1个小时.对于近期需要大量修改记录的情况来说这样生 ...

  2. elementUI中page(分页)的使用方法

    HTML部分 <!-- 快捷键 page-div --> <el-pagination background layout="sizes,prev, pager, next ...

  3. 第六章:Django 综合篇 - 3:使用MySQL数据库

    在实际生产环境,Django是不可能使用SQLite这种轻量级的基于文件的数据库作为生产数据库.一般较多的会选择MySQL. 下面介绍一下如何在Django中使用MySQL数据库. 一.安装MySQL ...

  4. 14. Fluentd输出插件:out_forward用法详解

    out_forward是一个带缓存的输出插件,用于向其他节点转发日志事件,并支持转发节点之间的负载均衡和自动故障切换. out_forward支持至多一次和至少一次传输模式,默认为至多一次. out_ ...

  5. localStorage概要

    在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localSt ...

  6. jquery修改图片src

    jquery修改图片src,会重新请求此图片url,重新加载图片.方法如下: $("#<img的id>").attr("src", src_valu ...

  7. 实现fastdfs防盗链功能

    目录 1.背景 2.实现原理 2.1 开启防盗链 2.2 重启 nginx 2.3 Java代码生成token 1.token生成规则 2.java生成token 3.测试 3.1 带正确token访 ...

  8. MatrixOne从入门到实践08——SSB性能测试

    MatrixOne从入门到实践--SSB性能测试 SSB 星型模式基准测试是 OLAP 数据库性能测试的常用场景,通过本篇教程,您可以了解到如何在 MatrixOne 中实现 SSB 测试. 测试环境 ...

  9. 【linux】 第1回 linux运维基础

    目录 1. 运维的本质 2. 电脑与服务器 2.1 电脑的种类 2.2 服务器种类 2.3 服务器品牌 2.4 服务器尺寸 2.5 服务器内部组成 3. 磁盘阵列 4. 系统简介 5. 虚拟化 6. ...

  10. 动态搜索图书:可以按书名、作者、出版社以及价格范围进行搜索。(在IDEA中mybatis)

    中午找了好久.好多人写的都驴头不对马嘴.自己实现后.才发现是真的不麻烦.也不知道人家咋想的.写的死麻烦还没用.老是搜出sql语句写死的.我要的是动态滴.自己写出后.总结了一下 1.按照书名.作者.出版 ...