自定义反向工程模板

注意

此功能已在 EF Core 7 中添加。

进行逆向工程时,Entity Framework Core 努力搭建可用于各种应用类型的良好通用代码,并使用 常见编码约定 实现一致的外观和熟悉的感觉。 但是,有时需要更专用的代码和替代编码样式。 本文介绍如何使用 T4 文本模板自定义基架代码。

先决条件

本文假设你熟悉 EF Core 中的逆向工程。 如果没有,请在继续之前查看该文章。

添加默认模板

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

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

dotnet new install Microsoft.EntityFrameworkCore.Templates

现在可以将默认模板添加到项目。 为此,请从项目目录运行以下命令。

dotnet new ef-templates

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

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

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

提示

(而不是 .tt) 使用 .t4 扩展来防止 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 #>>();
<#
    }

将 List 替换为 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

查看更改的另一种方法是下载两个版本的 Microsoft。NuGet 中的 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 #>
<#
        }
    }
#>
```