사용자 지정 리버스 엔지니어링 템플릿

참고 항목

이 기능은 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 확장은 Visual Studio가 템플릿을 변환하지 못하도록 하기 위해 .tt 대신 사용됩니다. 템플릿은 대신 EF Core에 의해 변환됩니다.

T4 소개

DbContext.t4 템플릿을 열고 해당 내용을 검사해 보겠습니다. 이 파일은 T4 텍스트 템플릿입니다. T4는 .NET을 사용하여 텍스트를 생성하는 언어입니다. 다음 코드는 설명용으로만 사용됩니다. 파일의 전체 내용을 나타내지 않습니다.

Important

특히 코드를 생성하는 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 템플릿 내의 Host 속성을 사용하여 EF Core 서비스에 액세스할 수 있도록 하는 hostSpecific="true"를 지정합니다.
assembly 템플릿을 컴파일하는 데 필요한 어셈블리 참조를 추가합니다.
parameter 템플릿을 변환할 때 EF Core에서 전달할 매개 변수를 선언합니다.
import 지시문을 사용하는 C#와 마찬가지로 는 네임스페이스를 템플릿 코드의 범위로 가져옵니다.

지시문 뒤에 오는 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 #>>();
<#
    }

목록을 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를 기반으로 해당 템플릿의 복사본이 만들어집니다. 버그가 수정되고 이후 버전의 EF Core에서 기능이 추가되면 템플릿이 만료될 수 있습니다. EF Core 템플릿에서 변경한 내용을 검토하고 사용자 지정 템플릿에 병합해야 합니다.

EF Core 템플릿의 변경 내용을 검토하는 한 가지 방법은 git을 사용하여 버전 간에 비교하는 것입니다. 다음 명령은 EF Core 리포지토리를 복제하고 버전 7.0.0과 8.0.0 간에 이러한 파일의 diff를 생성합니다.

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 #>
<#
        }
    }
#>
```