在 EF Core 7 中实现强类型 ID

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 本文主要介绍 DDD 中的强类型 ID 的概念,及其在 EF 7 中的实现,以及使用 LessCode.EFCore.StronglyTypedId 这种更简易的上手方式。

背景

在杨中科老师 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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
5月前
|
开发框架 缓存 .NET
【Entity Framework】EF中DbSet类详解
【Entity Framework】EF中DbSet类详解
90 1
【Entity Framework】EF中DbSet类详解
|
5月前
|
存储 SQL 开发框架
【Entity Framework】如何使用EF中的生成值
【Entity Framework】如何使用EF中的生成值
40 0
|
5月前
|
SQL 开发框架 .NET
【Entity Framework】聊聊EF中复杂查询运算符
【Entity Framework】聊聊EF中复杂查询运算符
116 0
|
5月前
|
开发框架 .NET 数据库
【Entity Framework】EF中SaveChanges如何使用
【Entity Framework】EF中SaveChanges如何使用
44 0
|
5月前
|
SQL API 数据库
【Entity Framework】EF配置文件设置详解
【Entity Framework】EF配置文件设置详解
46 0
|
5月前
|
存储 SQL API
【Entity Framework】EF中实体属性
【Entity Framework】EF中实体属性
36 0
|
5月前
|
SQL 数据库
【Entity Framework】如何理解EF中的级联删除
【Entity Framework】如何理解EF中的级联删除
51 0
|
5月前
|
存储 SQL 开发框架
【Entity Framework】你要知道EF中功能序列与值转换
【Entity Framework】你要知道EF中功能序列与值转换
36 0
|
5月前
|
SQL 数据库连接 数据库
【Entity Framework】EF连接字符串和模型
【Entity Framework】EF连接字符串和模型
33 0
|
SQL 存储 数据处理
5.1EF Core原理
对普通集合使用where等方法查询出来的返回值为IEnumerable类型 但是对DbSet使用用where等方法出查询出来的返回值为IQueryable类型