自定义反向工程模板

注意

EF Core 7 中添加了此功能。

进行反向工程时,Entity Framework Core 致力于搭建可用于各种应用类型的良好且通用的基架代码,并使用常见的编码规范来保持一致的外观和熟悉的感觉。 但有时,我们追求更专用的代码和可选编码样式。 本文介绍如何使用 T4 文本模板来自定义基架代码。

先决条件

本文假设你熟悉 EF Core 中的反向工程。 如果你不熟悉,请先参阅此文章然后再继续。

正在添加默认模板

自定义基架代码的第一步是将默认模板添加到项目。 默认模板是在进行反向工程时 EF Core 在内部使用的模板。 这些模板提供了开始自定义基架代码的起点。

首先,为 dotnet new 安装 EF Core 模板包:

dotnet new install Microsoft.EntityFrameworkCore.Templates

现在可以将默认模板添加到项目。 通过从项目目录运行以下命令来执行此操作。

dotnet new ef-templates

此命令会将以下文件添加到项目。

  • CodeTemplates/
    • EFCore/
      • DbContext.t4
      • EntityType.t4

DbContext.t4 模板用于为数据库搭建 DbContext 类基架,EntityType.t4 模板则用于为数据库中的每个表和视图搭建实体类型类基架。

提示

使用 .t4 扩展(而不是 .tt)来阻止 Visual Studio 转换这些模板。 模板将改为由 EF Core 转换。

T4 简介

让我们打开 DbContext.t4 模板并检查其内容。 此文件是一个 T4 文本模板。 T4 是一种使用 .NET 生成文本的语言。 以下代码仅用于说明目的,不表示文件的完整内容。

重要

T4 文本模板(尤其是生成代码的模板)在没有语法突出显示的情况下可能很难阅读。 如有必要,请搜索支持 T4 语法突出显示的代码编辑器的扩展。

<#@ template hostSpecific="true" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #>
<#@ parameter name="NamespaceHint" type="System.String" #>
<#@ import namespace="Microsoft.EntityFrameworkCore" #>
<#
    if (!string.IsNullOrEmpty(NamespaceHint))
    {
#>
namespace <#= NamespaceHint #>;

<#@ 开头的前几行称为指令, 会影响模板如何转换。 下表简要介绍了所使用的每种指令。

指令 说明
template 指定 hostSpecific=“true”,该指令将允许使用模板中的 Host 属性来访问 EF Core 服务。
assembly 添加编译该模板所需的程序集引用。
parameter 声明将在转换模板时由 EF Core 传入的参数。
import 与 C# using 指令类似,将命名空间引入模板代码的作用域。

在指令之后,DbContext.t4 的下一部分称为控制块。 标准控制块以 <# 开头,以 #> 结尾。 其中的代码将在转换模板时执行。 有关控制块内可用的属性和方法的列表,请参阅 TextTransformation 类。

控制块之外的任何内容将被直接复制到模板输出。

表达式控制块以 <#= 开头。 将对其中的代码进行计算,并将结果添加到模板输出中。 这些类似于 C# 内插字符串参数。

有关 T4 语法的更详细且完整的说明,请参阅编写 T4 文本模板

自定义实体类型

让我们来看看自定义模板是怎样的。 默认情况下,EF Core 为集合导航属性生成以下代码。

public virtual ICollection<Album> Albums { get; } = new List<Album>();

对于大多数应用程序来说,使用 List<T> 是很好的默认值。 但是,如果使用基于 XAML 的框架(如 WPF、WinUI 或 .NET MAUI),则用户通常希望改用 ObservableCollection<T> 来启用数据绑定。

打开 EntityType.t4 模板并找到它生成 List<T> 的位置。 它的外观如下所示:

    if (navigation.IsCollection)
    {
#>
    public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new List<<#= targetType #>>();
<#
    }

使用 ObservableCollection 替换列表。

public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new ObservableCollection<<#= targetType #>>();

我们还需要向基架代码添加 using 指令。 用法在模板顶部附近的列表中指定。 添加 System.Collections.ObjectModel 到列表中。

var usings = new List<string>
{
    "System",
    "System.Collections.Generic",
    "System.Collections.ObjectModel"
};

通过使用反向工程命令测试这些更改。 命令会自动使用项目中的模板。

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" Microsoft.EntityFrameworkCore.SqlServer

如果之前已运行该命令,请添加 --force 选项以覆盖现有文件。

如果已正确执行所有操作,则集合导航属性现在应使用 ObservableCollection<T>

public virtual ICollection<Album> Albums { get; } = new ObservableCollection<Album>();

正在更新模板

添加默认模板到项目时,会基于那个版本的 EF Core 创建一个副本。 由于 bug 已修复,并且相关功能会在 EF Core 的后续版本中添加,因此模板可能已过期。 应查看 EF Core 模板中的更改,并将其合并到自定义模板中。

查看对 EF Core 模板的更改的一种方法是使用 git 在版本之间进行比较。 以下命令将克隆 EF Core 存储库,并生成版本 7.0.0 和 8.0.0 之间这些文件的差异。

git clone --no-checkout https://github.com/dotnet/efcore.git
cd efcore
git diff v7.0.0 v8.0.0 -- src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt

查看更改的另一种方法是从 NuGet 下载 Microsoft.EntityFrameworkCore.Templates 的两个版本,提取其内容(可以将文件扩展名更改为 .zip),并比较这些文件。

在添加默认模板到新项目之前,请记得更新到最新的 EF Core 模板包。

dotnet new update

高级用法

忽略输入模型

参数 ModelEntityType 表示映射到数据库的一种可能方式。 可以选择忽略或更改部分模型。 例如,我们提供的导航名称可能并不理想,并且可以在搭建代码基架时用自己的名称替换。 其他内容(如约束名称和索引筛选器)仅供迁移使用,如果不打算对基架代码使用迁移,则可以安全地从模型中省略。 同样的,如果应用不使用序列或默认约束,则可能希望省略它们。

进行这种高级更改时,只需确保生成的模型与数据库相兼容。 查看由 dbContext.Database.GenerateCreateScript() 生成的 SQL 是验证它的好方法。

实体配置类

对于大型模型而言,DbContext 类的 OnModelCreating 方法可能会变得很大,难以管理。 解决此问题的一种方法是使用 IEntityTypeConfiguration<T> 类。 有关这些类的详细信息,请参阅“创建和配置模型”。

若要搭建这些类的基架,可以使用名为 EntityTypeConfiguration.t4 的第三个模板。 与 EntityType.t4 模板一样,它用于模型中的每个实体类型,并使用 EntityType 模板参数。

搭建其他类型文件的基架

EF Core 中反向工程的主要目的是搭建 DbContext 和实体类型基架。 但是,没有任何工具需求你实际搭建基架代码。 例如,可以改用 Mermaid 来搭建实体关系图基架。

<#@ output extension=".md" #>
<#@ assembly name="Microsoft.EntityFrameworkCore" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #>
<#@ parameter name="Model" type="Microsoft.EntityFrameworkCore.Metadata.IModel" #>
<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="Microsoft.EntityFrameworkCore" #>
# <#= Options.ContextName #>

```mermaid
erDiagram
<#
    foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType()))
    {
#>
    <#= entityType.Name #> {
    }
<#
        foreach (var foreignKey in entityType.GetForeignKeys())
        {
#>
    <#= entityType.Name #> <#= foreignKey.IsUnique ? "|" : "}" #>o--<#= foreignKey.IsRequired ? "|" : "o" #>| <#= foreignKey.PrincipalEntityType.Name #> : "<#= foreignKey.GetConstraintName() #>"
<#
        }

        foreach (var skipNavigation in entityType.GetSkipNavigations().Where(n => n.IsLeftNavigation()))
        {
#>
    <#= entityType.Name #> }o--o{ <#= skipNavigation.TargetEntityType.Name #> : <#= skipNavigation.JoinEntityType.Name #>
<#
        }
    }
#>
```