背景
在杨中科老师 B 站的.Net Core 视频教程其中 DDD 部分讲到了强类型 ID(Strongly-typed-id)的概念,也叫受保护的密钥(guarded keys)当时在 .NET 中的 DDD 实现是个悬而未决的问题,之后我也一直在寻找相关的实现方案。
非常高兴 .NET 7 的更新带来的 EF Core 7.0 的新增功能中,就包含了改进的值生成这一部分,在自动生成关键属性的值方面进行了两项重大改进。
下面我们通过几个例子来了解这部分的内容,以及如何更简便的实现强类型。
强类型 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);
}
}
进行数据库迁移,我们可以发现其创建的数据库表情况如下:
然后在 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));
其执行结果如下:
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'.
强类型 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.EFCore
Install-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