Entity Framework Core (EFCore) Reverse Engineering und property renaming / Eigenschaften umbenennen

Kategorie: c# | Development | Techblog

Entity Framework Core (EFCore) unterstützt das Umbenennen von Eigenschaften anhand von T4 Code-Templates. Wer bisher das Reverse Engineering mit EF Core Power Tools gemacht hat und von dessen Renaming-Feature gebrauch gemacht hat, wurde Anfang des Jahres 2024 von einer Warnung überrascht:

„Property renaming (experimental) is no longer available. See GitHub issue #2171.“

In dem GitHub-Issue wurde auf die T4-Code Templates hingewiesen. Wenn man sich damit nun voller Enttäuschung auseinandersetzt, wird eigentlich schnell klar, dass die T4-Code Templates mehr Möglichkeiten bieten, als das „stumpfe“ umbenennen von fixen Eigenschaftennamen, wie bisher.

Erik, der Autor der Extension „EF Core Power Tools“ hat mit Veröffentlichung des Updates ein Umbenennungs-Beispiel veröffentlicht: github.com

Zu sehen sind hier 2 Dateien:

  • DbContext.t4
  • EntityType.t4

Die Namen sind recht intuitiv – DbContext ist das Template für die generierten Context-Klassen, EntityType für die Entities.

Um diese im Projekt zu erstellen mit dem Terminal folgenden Befehl ausführen um die Code-Templates zu installieren: (Quelle: Microsoft.com)

dotnet new install Microsoft.EntityFrameworkCore.Templates

Anschließend im Terminal in das Verzeichnis des Projekts gehen und die Templates hinzufügen:

dotnet new ef-templates

Nun wurden im Projekt unter dem Ordner „CodeTemplates/EFCore“ die entsprechenden Templates hinzugefügt.

Um in einer Entität nun z.B. Eigenschaften systematisch umbenennen zu können müssen folgende Zeilen der EntityType.t4 hinzugefügt werden:

...
// Template version: 703 - please do NOT remove this line
if (EntityType.IsSimpleManyToManyJoinEntityType())
{
    // Don't scaffold these
    return "";
}

// Property renaming list { BEGIN
var propertyNames = new List<(string EntityName, string PropertyName, string NewPropertyName)>
{
    ("EntityNameA", "PropertyAName", "PropertyANewName"),
    ("EntityNameB", "PropertyBName", "PropertyBNewName"),
};
// Property renaming list END }

var services = (IServiceProvider)Host;
var annotationCodeGenerator = services.GetRequiredService<IAnnotationCodeGenerator>();
var code = services.GetRequiredService<ICSharpHelper>();
...

Zur Orientierung sind ein Paar Zeilen davor und danach mit eingefügt. Die eigentliche Umbenennung findet dann weiter unten satt:

...
public partial class <#= EntityType.Name #>
{
    <# var firstProperty = true; foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1))
...

        var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !property.ClrType.IsValueType;
        var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType;
        var renameTuple = propertyNames.SingleOrDefault(
            t => (
                t.EntityName == EntityType.Name || (
                    entityNames.ContainsKey(EntityType.Name) && t.EntityName == entityNames[EntityType.Name]
                )
            ) && t.PropertyName == property.Name);
        var propertyName = renameTuple.NewPropertyName ?? property.Name;
#>
    public <#= code.Reference(property.ClrType) #><#= needsNullable ? "?" : "" #> <#=propertyName #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#

Halte ausschau nach der Schleife „forreach (var property in EntityType.GetPropeties()….“. Nicht weit unterhalb dieser Zeilen, innerhalb der Schleife werden die EIgenschaften erstellt.

Wie man Navigations-Eigenschaften umbennent hat Erik in seinem Beispiel gezeigt (siehe Oben). Beachte dabei die Änderung auch in dem DbContext.t4-Template.

Allerdings war das Beispiel für eine recht komplexe Datenbank eines Projektes nicht ausreichend, da ich zum einen einige Tabellen / Entitäten in Gänze umbennen wollte und hierbei Relationen gab bei denen die FluentApi der generischen „HasForeignKey()“-Methode ein Typenargument voranstellt. Die FluentApi bekommt allerdings nichts von der Umbenennung mit und der Code wird nicht vom T4-Template beeinflusst, sondern via „code.Fragement()“ aus dem FluentApiCall-Objekt erstellt.

Auf der Basis des Templates von Erik möchte ich hier folgende Ergänzungen beisteuern, welche sowohl in der DbContext.t4, als auch in der EntityType.t4 anwendung finden:

var entityNames = new Dictionary<string, string>
{
    { "Altetabelle", "NeueTabelle" },
    { "Altetabellezwei", "NeueTabelleZwei" },
};
...

<#
    foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType()))
    {
        var entityName = entityNames.ContainsKey(entityType.Name) ? entityNames[entityType.Name] : entityType.Name;
        var entityDbSetName = entityType.GetDbSetName();
        var entityDbSetNameNew = entityNames.ContainsKey(entityDbSetName) ? entityNames[entityDbSetName] : entityDbSetName;
#>
    public virtual DbSet<<#= entityName #>> <#= entityDbSetNameNew #> { get; set; }

...
...

        var anyEntityTypeConfiguration = false;
        var entityName = entityNames.ContainsKey(entityType.Name) ? entityNames[entityType.Name] : entityType.Name;
#>
        modelBuilder.Entity<<#= entityName #>>(entity =>
        {
<#
        var key = entityType.FindPrimaryKey();

...
...
        // Renaming foreign keys and relation-properties
        var renameTuple = propertyNames.SingleOrDefault(t =>
            t.EntityTypeName == entityName &&
            (t.DependentToPrincipalName == foreignKey.DependentToPrincipal.Name 
                || (entityNames.ContainsKey(foreignKey.DependentToPrincipal.Name) 
                    && t.DependentToPrincipalName == entityNames[foreignKey.DependentToPrincipal.Name])
            ) && (t.PrincipalToDependentName == foreignKey.PrincipalToDependent.Name 
                || (entityNames.ContainsKey(foreignKey.PrincipalToDependent.Name) 
                    && t.PrincipalToDependentName == entityNames[foreignKey.PrincipalToDependent.Name])));

var dependentToPrincipalName = renameTuple.NewDependentToPrincipalName ?? foreignKey.DependentToPrincipal.Name;
var principalToDependentName = renameTuple.NewPrincipalToDependentName ?? foreignKey.PrincipalToDependent.Name;

            // Renaming property names (type) in FluentApi
            for (int i = 0; i < foreignKeyFluentApiCalls.TypeArguments.Count; i++)
            {
				var type = foreignKeyFluentApiCalls.TypeArguments[i];
				if (entityNames.ContainsKey(type))
					foreignKeyFluentApiCalls.TypeArguments[i] = entityNames[type];
            }
#>
            entity.HasOne(d => d.<#= dependentToPrincipalName #>).<#= foreignKey.IsUnique ? "WithOne" : "WithMany" #>(<#= foreignKey.PrincipalToDependent != null ? $"p => p.{principalToDependentName}" : "" #>)<#= code.Fragment(foreignKeyFluentApiCalls, indent: 4) #>;

...

    var entityName = entityNames.ContainsKey(EntityType.Name) ? entityNames[EntityType.Name] : EntityType.Name;
    var services = (IServiceProvider)Host;
    var annotationCodeGenerator = services.GetRequiredService<IAnnotationCodeGenerator>();
    var code = services.GetRequiredService<ICSharpHelper>();

...

#>
public partial class <#= entityName #>
{
<#

...

Wenn nun das Scaffolding ausgeführt wird, werden die Context-Klassen und Entities nach den Templates erstellt.