Blog

Erweitern von Entity Framework Core

Victor de Baare

Victor de Baare

Aktualisiert Oktober 15, 2025
10 Minuten

Erweitern von Entity Framework Core

Entity Framework Core bietet ein umfassendes Framework, mit dem Sie Ihr Datenbankschema erstellen und dann Ihre Daten in dieser Datenbank speichern und darauf zugreifen können. Was aber, wenn Sie mehr tun möchten als das, was Entity Framework Core von Haus aus bietet? Was ist, wenn Sie einen Sonderfall haben, der unterstützt werden muss? Die Erweiterung von Entity Framework Core könnte eine Lösung sein.

Was ist Entity Framework Core?

Entity Framework Core (EF core) ist ein von Microsoft unterstütztes Open-Source Object-Relational Mapping (ORM) Framework. Wie der Name schon sagt, hilft es Entwicklern bei der Persistierung von und dem Zugriff auf Daten, die sich in einer Datenbank befinden, ohne dass sie diese aus dem DataSet der Datenbank in .NET-Objekte konvertieren müssen. Außerdem hilft es bei der Erstellung eines Datenbankschemas, das auf den .NET-Objekten basiert, oder umgekehrt bei der Erstellung von .NET-Objekten auf der Grundlage Ihres Datenbankschemas. Zusammenfassend lässt sich sagen, dass ein Großteil des Codes für den Datenzugriff und die Datenhaltung entfällt und der Entwickler mehr Zeit hat, sich auf wichtigere Bereiche des Codes zu konzentrieren.

Wir werden uns nun mit der Persistenz von Daten in Ihrer Datenbank, insbesondere einer SQL-Datenbank, beschäftigen. Wenn Sie sich entscheiden, Daten in Ihrer Datenbank zu speichern, wird EF core dies mit einer SQL-Anweisung zum Einfügen, Aktualisieren oder Löschen tun. Oftmals ist diese Vorgehensweise "gut genug", aber in manchen Situationen sollten Sie stattdessen eine Merge-Anweisung verwenden. Zum Beispiel wird bei jeder Einfüge-, Aktualisierungs- oder Löschanweisung ein Roundtrip zur Datenbank durchgeführt. Bei wenigen Datensätzen stellt dies kein Problem dar. Wenn die Anzahl der Datensätze steigt, verlangsamen die zahlreichen Roundtrips die Anwendung. Um diese Leistungseinbußen zu vermeiden, könnte eine Merge-Anweisung verwendet werden. Eine Merge-Anweisung würde für jeden Objekttyp, der eingefügt, aktualisiert oder gelöscht werden muss, unabhängig von der Anzahl der Datensätze einen einzigen Roundtrip zur Datenbank erzeugen.

Um zu vermeiden, dass Sie den Code für jedes Objekt wieder selbst schreiben müssen, können Sie eine Erweiterung für EF core schreiben. Auf diese Weise brauchen Sie den Code nur einmal zu schreiben und können danach EF core die schwere Arbeit überlassen. In diesem Artikel gehen wir näher darauf ein, wie Sie EF Core erweitern können. Im nächsten Artikel werden wir uns eingehender mit der Verwendung von Merge-Anweisungen beschäftigen. Dazu müssen Sie zunächst wissen, wie eine Migration in EF Core funktioniert.

Migrationen

EF core verwendet Migrationen, um das Datenbankschema auf der Grundlage der vom Entwickler vorgenommenen Datenmodelländerungen zu aktualisieren. EF core vergleicht die vom Entwickler vorgenommenen Änderungen am Datenmodell mit einem Snapshot des alten Modells, der EF core bekannt ist. Auf der Grundlage des Snapshots wird eine Migrationsdatei erstellt, die die Operationen beschreibt, die erforderlich sind, um vom alten Modell zum neuen Modell zu wechseln. Die einzelnen Migrationsdateien werden in einer Historientabelle in der Datenbank gespeichert. Auf diese Weise kann EF core verfolgen, welche Migrationen durchgeführt wurden und welche nicht. Das ist wichtig, wenn Sie Ihre Datenbank aktualisieren möchten und mehrere Migrationen durchführen müssen.

Die Migration erfolgt in zwei Schritten: die Erstellung einer Migrationsdatei auf der Grundlage Ihrer Änderungen und die Aktualisierung Ihres Datenbankschemas. Um eine Migrationsdatei zu erstellen, können Sie die Befehle der .NET EF-Konsole verwenden. Dies führt zum Aufruf des CSharp-Teils der Migration, des CSharpMigrationGenerators. Wir werden CSharpMigrationGenerator später in diesem Artikel besprechen. Der CSharpMigrationGenerator generiert Migrationsvorgänge auf der Grundlage der vorgenommenen Änderungen. Die Änderungen werden in einer migration.cs-Datei gespeichert, in der Sie zusätzliche Operationen oder zusätzliches benutzerdefiniertes SQL hinzufügen können. Die Aktualisierung der Datenbank wird oft während des Starts Ihrer Anwendung durchgeführt. Wir werden zunächst erörtern, wie wir die SQL-Skripterstellung von EF core erweitern können.

Erweitern der MigrationBuilder API.

Der MigrationBuilder erstellt die Migration und enthält viele verschiedene Operationen. Aber er kann nicht alles enthalten, was ein Entwickler tun möchte. Um die Möglichkeit zu schaffen, eigene Operationen zu erstellen, kann der MigrationBuilder erweitert werden! Sie können die Methodesql() verwenden, um ein eigenes kleines Stück Code in eine Migration zu schreiben. Die Methodesql()kann nützlich sein, wenn ein Entwickler einige Datensätze vor oder nach der Durchführung einer Migration aktualisieren möchte. Eine weitere Möglichkeit ist das Schreiben eigener benutzerdefinierter Migrationsoperationen zur Erweiterung der Migrationen.

    public class CustomMigrationSqlGenerator : SqlServerMigrationsSqlGenerator
    {
        public CustomMigrationSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, ICommandBatchPreparer commandBatchPreparer) : base(dependencies, commandBatchPreparer) { }

        protected override void Generate(MigrationOperation operation, IModel model, MigrationCommandListBuilder builder)
        { 
            case CreateMergeOperation createoperation:
                builder.AppendLine("---Write your SQL code here");
                builder.EndCommand();
                break;
            case DropMergeOperation dropoperation:
                builder.AppendLine("---Write your SQL code here");
                builder.EndCommand(); break;
            default:
                base.Generate(operation, model, builder); break; 
        } 
    }

Wenn Sie sich entscheiden, Ihre eigenen benutzerdefinierten Migrationsvorgänge zu erstellen, müssen Sie zunächst Ihren eigenen CustomMigrationSqlGenerator deklarieren und ihn von SqlServerMigrationSqlGenerator erben lassen.

Der nächste Schritt ist die Registrierung Ihrer benutzerdefinierten Implementierung des SQL-Generators in Ihrem EF core DbContext. Nachdem Sie Ihre benutzerdefinierte Implementierung registriert und eine Migration durchgeführt haben, wird Ihr benutzerdefinierter Generator anstelle des Standardgenerators für die Generierung Ihres Migrationscodes verwendet.

protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
     .UseSqlServer(_connectionString)
     .ReplaceService<IMigrationsSqlGenerator, MyMigrationsSqlGenerator>();

Jetzt sind alle notwendigen Schritte für die Implementierung Ihrer eigenen benutzerdefinierten Operationen erledigt. Wenn zum Beispiel eine Merge-Operation in der Datenbank erstellt werden muss, können Sie eine CreateMergeOperation erstellen und diese der Methode CustomMigrationSqlGenerator.Generate hinzufügen. Dann können Sie den Code zu einer bestehenden Migration hinzufügen, so dass die Operation dem SQL-Skript hinzugefügt wird, wenn die Migration ausgeführt wird.

///Generated Migration.cs
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateMerge(‘TableName’, (columnsBuilder) => new {...});
}
///CustomMigrationBuilderExtensions.cs
public static OperationBuilder<CreateMergeOperation> CreateMerge(
   this MigrationBuilder migrationBuilder,
   string tableName,
   Func<ColumnsBuilder, TColumns> columns)
{
     var operation = new CreateMergeOperation(tableName, columns);
     migrationBuilder.Operations.Add(operation);
     return new OperationBuilder<CreateMergeOperation>(operation);
}

Bei der Implementierung muss der Entwickler nach dem ersten Teil der Migration noch benutzerdefinierten Code hinzufügen. Andernfalls würde die Migration nicht wissen, dass eine weitere Operation ausgeführt werden soll. Eine bessere Implementierung wäre es, Anmerkungen zu den Entitäten hinzuzufügen, für die Sie benutzerdefinierte Operationen erhalten möchten. Während der Generierung der Operationen können Sie überprüfen, ob eine Entität eine bestimmte Anmerkung hat. Wenn die Anmerkung vorhanden ist, können Sie den benutzerdefinierten Code ausführen.

Der SQL-Generator funktioniert nur im zweiten Teil der Migrationen: der SQL-Skripterstellung. Am besten wäre es, wenn Sie im ersten Teil der Migration automatisch benutzerdefinierte Operationen auf der Grundlage von Anmerkungen hinzufügen würden. Auf diese Weise müssten Sie nichts weiter tun, als in der Konfiguration des Modells eine Anmerkung hinzuzufügen. Anschließend könnten Sie die neuen Operationen in der Migrationsklasse sehen, die im ersten Schritt generiert wird.

Um dies zu erreichen, müssten Sie weitere Dienste erweitern, die für die Migration verwendet werden. Die MigrationsModelDiffer.cs und die CSharpMigrationOperationGenerator.cs sind die beiden Dienste, die Sie erweitern müssen. Wie Sie diese Dienste erweitern können, erfahren Sie in den folgenden Abschnitten.

MigrationsModelDiffer

Wie der Name schon sagt, prüft dieser Teil der Migration, ob es Unterschiede zwischen dem neuen Modell und dem alten Modell gibt. Das Schöne daran ist, dass Sie die Prüfung auf mögliche benutzerdefinierte Anmerkungen für die Unterschiede einbeziehen können, wenn Sie benutzerdefinierten Code einfügen möchten, wenn eine Anmerkung vorhanden ist. Sie können diese diff-Methode auch verwenden, um den benutzerdefinierten Code zu entfernen, wenn die Anmerkung nicht mehr vorhanden ist. Für die Implementierung müssten Sie eine Klasse erstellen, die von der Klasse MigrationsModelDiffer.cs erbt und dann die folgende Methode überschreiben:

/// CustomMigrationsModelDiffer.cs
public override IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source, IRelationalModel? target)

Die Methode wird von der Migration aufgerufen, um die Unterschiede zwischen den Ziel- und Quellentitäten zu ermitteln. Indem wir dies überschreiben, können wir das Ziel und die Quelle auf die Annotation überprüfen und wenn Änderungen vorhanden sind, können wir sie anweisen, eine Operation zu erstellen, um das Datenbankschema mit den gewünschten Änderungen zu aktualisieren. Die Operationen, die hier erstellt werden, werden an den nächsten Schritt der Migration weitergegeben: den CSharpMigrationOperationGenerator.

Ein Beispiel für die Implementierung der benutzerdefinierten Vorgänge lautet wie folgt:

public override IReadOnlyList<MigrationOperation> GetDifferences(IRelationalModel? source, IRelationalModel? target)
{
    var sourceTypes = GetEntityTypesContainingMergeAnnotation(source);
    var targetTypes = GetEntityTypesContainingMergeAnnotation(target);
    var diffContext = new DiffContext();
    var customOperations = DiffCollection(source, target, diffContext, Diff, Add, Remove, (x, y, diff) => x.Name.Equals(y.Name, StringComparison.CurrentCultureIgnoreCase));

    return base.GetDifferences(source, target).Concat(customOperations).ToList();
}

DiffCollections ist eine interne API-Methode, die dafür sorgt, dass eine Operation nur einmal für die Kombination aus Ziel und Quelle erstellt wird. Die Parameter add und remove in der DiffCollection sind die Funktionen, die das Wissen darüber enthalten, wie die benutzerdefinierten Operationen erstellt werden.

Der letzte Schritt, den wir tun müssen, ist die Registrierung von ModelDiffer in der Servicesammlung. Dieser Dienst muss auch beim DbContext registriert werden. Von hier aus können wir den vorherigen Code erweitern, um den ModelDiffer-Dienst einzubeziehen.

protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
   .UseSqlServer(_connectionString)
   .ReplaceService<IMigrationsModelDiffer, CustomMigrationsModelDiffer>()
   .ReplaceService<IMigrationsSqlGenerator, MyMigrationsSqlGenerator>();

CSharp Migration Operation Generator

Der CSharpMigrationOperationGenerator liefert den .NET-Code, der in der Migrationsklasse geparst wird. Mit den neuen Operationen kann der Csharp-Generator nun so erweitert werden, dass er weiß, was zu tun ist, wenn er auf eine solche Operation trifft. Wichtig dabei ist, dass die Migrationen in zwei Teilen durchgeführt werden, die beide zu unterschiedlichen Zeiten ausgeführt werden. Zum Beispiel wird die Migration des Csharp-Codes während der Entwurfszeit durchgeführt. Die SQL-Migration wird oft während der Laufzeit durchgeführt.

Für die SQL-Migration bedeutet dies, dass wir die DbContext OnConfiguration-Methode verwenden können, um die benötigten Überschreibungen zu registrieren und das richtige SQL-Skript zu erstellen. Der CsharpMigrationOperationGenerator, der während der Entwurfszeit läuft, ist anders implementiert. Um sicherzustellen, dass Ihr Kunde CsharpMigrationOperationGenerator aufrufen kann, passen Sie das importierte Microsoft.EntityFrameworkCore.Design NuGet-Paket an.

Wenn Sie dieses Paket importieren, sorgt es automatisch dafür, dass bestimmte Dienste zur Entwurfszeit für Ihren Code nicht sichtbar sind. Der CSharpMigrationOperationGenerator ist Teil der Dienste, die herausgefiltert werden. Um tatsächlich alle Dienste nutzen zu können, sollten Sie den Code in Ihrer .csproj-Datei ein wenig anpassen.

Wenn Sie zum Beispiel das Paket importieren, erhalten Sie folgendes:

<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive
    </IncludeAssets>
</PackageReference>

Nun müssen Sie dies wie folgt anpassen:

<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
    <PrivateAssets>all</PrivateAssets>
</PackageReference>

Sobald dies geschehen ist, können Sie den CSharpMigrationOperationGenerator mit Ihrem eigenen Code erweitern. Bevor wir mit der Erweiterung des Generators beginnen, müssen wir sicherstellen, dass der Generator während einer Migration gefunden werden kann. Für den SQL-Generator können wir die Methode OnConfiguration des DbContext verwenden. Da es sich beim CSharp-Generator um eine Designzeit-Implementierung handelt, wird die Lösung mit dem SQL-Generator nicht funktionieren. Eine Alternative dazu ist die Verwendung der DesignTimeServices.

Für DesignTimeServices gibt es in Entity Framework eine eigene Schnittstelle, die eingebunden werden muss. Die Schnittstelle stellt die

ConfigureDesignTimeServices(IServiceCollection)

Methode. EF core durchsucht das Startprojekt nach dieser Schnittstelle, um potenzielle Entwurfszeitdienste zu registrieren. Wenn Sie potenzielle Erweiterungen in einem NuGet-Paket ausliefern möchten, könnte das Scannen nach einer Schnittstelle ein Problem darstellen. Die Lösung für dieses Problem ist die Verwendung eines Assembly-Attributs in Ihrem Startup-Projekt, das den Namen der Klasse, die die Schnittstelle implementiert, und den vollständigen Namespace enthält.

[DesignTimeServicesReference("TypeName", "ForProvider")]

Der Parameter typename ist der assemblyqualifizierte Name, der der Servicesammlung hinzugefügt werden soll. Der Parameter ForProvider ist der Name des Anbieters, für den die DesignTimeServices verwendet werden sollen. Dieser Parameter kann gelöscht werden. Wenn er leer bleibt, wird der Dienst für alle vorhandenen Anbieter hinzugefügt. Nachdem die Schnittstelle eingerichtet ist, können wir mit der Implementierung des CSharp-Generators beginnen. Die Implementierung ist der des SQL-Generators sehr ähnlich. Wir werden die Methodeprotected virtual void Generate(MigrationOperation operation, IndentedStringBuilder builder)außer Kraft setzen. Der MigrationsModelDiffer übernimmt die Arbeit, um die benutzerdefinierten Operationen bereitzustellen, so dass der CSharp-Generator den CSharp-Code der Implementierung bereitstellen sollte. In diesem Fall, wenn wir eine Operation des TypsCreateMergeOperationerhalten, möchten wir den folgenden zusätzlichen Code ausführen:

private static void Generate(CreateMergeOperation operation, IndentedStringBuilder builder)
{
    builder.AppendLine($".CreateMerge(“);
    using(builder.Indent())
    {
        builder.AppendLine($".….");
     }
}

Wenn Sie eine neue Migration erstellen, werden Sie sehen, dass die Migration jetzt die Anweisung CreateMerge enthält.

Damit ist die gesamte Kette abgeschlossen. Wir können jetzt einfach eine Anmerkung zu den Entitäten in der Model Builder-Methode auf dem DbContext hinzufügen. Dies führt dazu, dass die Merge-Anweisungen bei der nächsten Migration generiert werden. Ein Beispiel für den Quellcode zur Erzeugung von Kundenvorgängen finden Sie hier: https://github.com/VictordeBaare/EntityFrameworkExtensions

Verfasst von

Victor de Baare

Victor de Baare is a developer with a deep passion for building systems that make work more efficient and straightforward. He thrives under pressure, persistently working towards his goals regardless of the challenge. He finds joy in whisky, food, gaming, and board games. He continues to inspire and innovate, making significant contributions to technology and efficiency.

Contact

Let’s discuss how we can support your journey.