在 EF Core 7 中实现强类型 ID

本文主要介绍 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);    }
}

进行数据库迁移,我们可以发现其创建的数据库表情况如下:

在 EF Core 7 中实现强类型 ID
数据库

然后在 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));

其执行结果如下:

在 EF Core 7 中实现强类型 ID
执行结果

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'.
在 EF Core 7 中实现强类型 ID
迁移报错

强类型 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.EFCoreInstall-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