在 EF Core 7 中实现强类型 ID

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在 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);
    


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

数据库

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

References

[1] .Net Core 视频教程: https://www.bilibili.com/video/BV1pK41137He/
[2] 改进的值生成: https://learn.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-7.0/whatsnew#improved-value-generation

以上是关于在 EF Core 7 中实现强类型 ID的主要内容,如果未能解决你的问题,请参考以下文章

EF Core 中实现 动态数据过滤器

如何在 .NET EF Core 中实现自引用多对多关系

EF Core 7.0 – JSON Column

如何让 EF Core 6 支持 DateOnly 类型

EF Core:实体类型“用户”需要定义主键

序列化 MirageJS 响应以适合 EF Core API 类型 List<>