カスタム リバース エンジニアリング テンプレート

Note

この機能は、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 テンプレートはデータベース内の各テーブルとビューのエンティティ型クラスをスキャフォールディングするために使用します。

ヒント

Visual Studio でテンプレートを変換できないようにするには、(.tt ではなく) .t4 拡張機能を使用します。 テンプレートは、代わりに 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> を使用することが適切です。 ただし、WPF、WinUI、.NET MAUI などの XAML ベースのフレームワークを使用している場合は、代わりに 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 ディレクティブを追加する必要があります。 usings は、テンプレートの上部付近のリストに指定します。 リストに 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 に基づいてそれらのコピーが作成されます。 EF Core の以降のバージョンでバグが修正され、機能が追加されると、テンプレートが古くなる可能性があります。 EF Core テンプレートで行われた変更点を確認して、自分のカスタマイズしたテンプレートにマージする必要があります。

EF Core テンプレートに加えられた変更点を確認する 1 つの方法は、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

変更点を確認するもう 1 つの方法は、NuGet から Microsoft.EntityFrameworkCore.Templates の 2 つのバージョンをダウンロードし、その内容を展開し (ファイル拡張子を.zip に変更できます)、それらのファイルを比較することです。

既定のテンプレートを新しいプロジェクトに追加する前に、必ず最新の EF Core テンプレート パッケージに更新してください。

dotnet new update

詳細な使用方法

入力モデルを無視する

ModelEntityType のパラメーターは、データベースにマッピングする 1 つの可能な方法を表します。 モデルの一部を無視または変更することができます。 たとえば、提供されるナビゲーション名が理想的でない場合があり、コードをスキャフォールディングするときに独自の名前に置き換えることができます。 制約名やインデックス フィルターなどの他のものは、Migrations でのみ使用され、スキャフォールディングされたコードで Migrations を使用する予定がない場合は、モデルから安全に省略できます。 同様に、シーケンスまたは既定の制約がアプリで使用されていない場合は省略できます。

このような高度な変更を行う場合は、結果のモデルがデータベースと互換性を保つようにします。 dbContext.Database.GenerateCreateScript() によって生成された SQL を確認することが、これを検証する良い方法です。

エンティティ構成クラス

大規模なモデルの場合、DbContext クラスの OnModelCreating メソッドが手に負えないほど大きくなる可能性があります。 これに対処する 1 つの方法は、IEntityTypeConfiguration<T> クラスを使用することです。 これらのクラスの詳細については、「モデルの作成と構成」を参照してください。

これらのクラスをスキャフォールディングするには、EntityTypeConfiguration.t4 という名前の 3 つ目のテンプレートを使用できます。 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 #>
<#
        }
    }
#>
```