Was ist neu in EF Core 9: Die Funktionen, die Sie kennen müssen

· 12 Min. Lesezeit

Entity Framework Core 9 wurde im November 2024 zusammen mit .NET 9 ausgeliefert, und nachdem ich viel Zeit damit verbracht habe, in mehreren Projekten damit zu arbeiten, kann ich sagen, dass es eine der bedeutendsten Versionen seit langem ist. Nicht, weil es das Rad neu erfindet, sondern weil es die Bereiche aufpoliert, in denen EF Core in der Vergangenheit die meisten Reibungspunkte verursacht hat – Abfrageübersetzung, Leistung und die Arbeit mit modernen Datenmustern.

In diesem Beitrag gehe ich auf die Funktionen ein, die den größten Einfluss auf meine tägliche Arbeit hatten. Wenn Sie noch EF Core 8 (oder sogar 7) verwenden, sollte Ihnen dies ein klares Bild davon geben, was Sie auf der anderen Seite des Upgrades erwartet.

EF Core 9 im .NET 9-Ökosystem

EF Core 9 zielt auf .NET 8 und .NET 9 ab, was bedeutet, dass Sie nicht unbedingt Ihre gesamte Anwendung auf .NET 9 aktualisieren müssen, um die meisten dieser Funktionen nutzen zu können. Allerdings sind einige der AOT- und Leistungsverbesserungen eng mit .NET 9-Laufzeitänderungen verknüpft, sodass Sie das Beste daraus machen können, wenn Sie den ganzen Weg gehen.

Die Veröffentlichung folgt dem von Microsoft festgelegten ungeraden/geraden Rhythmus: Veröffentlichungen mit ungeraden Nummern (wie .NET 9) sind Standard Term Support (STS) mit 18 Monaten Support, während gerade Veröffentlichungen (wie .NET 8) Long Term Support (LTS) sind. Berücksichtigen Sie dies bei der Planung Ihres Upgrade-Zeitplans.

LINQ-Übersetzungsverbesserungen

Hier werden die meisten Entwickler den Unterschied sofort spüren. EF Core 9 hat bei der Übersetzung von LINQ-Ausdrücken in SQL erhebliche Fortschritte gemacht, die tatsächlich Sinn machen.

Bessere GroupBy-Übersetzungen

Wenn Sie jemals eine GroupBy-Abfrage in EF Core geschrieben haben und dabei clientseitige Auswertungswarnungen oder bizarres SQL erhalten haben, kennen Sie den Schmerz. EF Core 9 verarbeitet einen viel breiteren Satz von GroupBy-Szenarien direkt in SQL.

var salesByCategory = await context.Products
    .GroupBy(p => p.Category.Name)
    .Select(g => new
    {
        Category = g.Key,
        TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
        AveragePrice = g.Average(p => p.Price),
        ProductCount = g.Count()
    })
    .OrderByDescending(x => x.TotalRevenue)
    .ToListAsync();

In früheren Versionen fielen Abfragen, die Aggregationen über Navigationseigenschaften innerhalb von GroupBy beinhalteten, manchmal auf die Clientauswertung zurück. EF Core 9 übersetzt dies sauber in eine einzelne SQL-Abfrage mit GROUP BY, SUM, AVG und COUNT.

Komplexe Projektionen und Unterabfragen

Auch verschachtelte Unterabfragen und komplexe Projektionen wurden erheblich verbessert. Betrachten Sie etwa Folgendes:

var orderSummaries = await context.Customers
    .Select(c => new CustomerSummaryDto
    {
        Name = c.FullName,
        TotalOrders = c.Orders.Count(),
        MostRecentOrder = c.Orders
            .OrderByDescending(o => o.OrderDate)
            .Select(o => new OrderBriefDto
            {
                Id = o.Id,
                Date = o.OrderDate,
                Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
            })
            .FirstOrDefault(),
        TopCategory = c.Orders
            .SelectMany(o => o.LineItems)
            .GroupBy(li => li.Product.Category.Name)
            .OrderByDescending(g => g.Count())
            .Select(g => g.Key)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 kann jetzt diesen gesamten Ausdruck in SQL übersetzen, ohne eine clientseitige Auswertung auszulösen. Die generierte Abfrage verwendet gegebenenfalls korrelierte Unterabfragen und seitliche Verknüpfungen, und der SQL-Plan ist erheblich effizienter als die Ergebnisse früherer Versionen.

Parametrisierte Primitivsammlungen

Eine der herausragenden LINQ-Verbesserungen ist die Möglichkeit, Sammlungen primitiver Werte direkt an Abfragen zu übergeben:

var statusFilter = new List<string> { "Active", "Pending", "Review" };

var filteredOrders = await context.Orders
    .Where(o => statusFilter.Contains(o.Status))
    .ToListAsync();

In EF Core 8 wurde dies mithilfe von IN-Klauseln mit inline-Werten übersetzt, was bedeutete, dass der Abfrageplan-Cache nicht wiederverwendet werden konnte, wenn sich die Liste änderte. EF Core 9 parametrisiert diese Sammlungen ordnungsgemäß und sendet sie als strukturierte Parameter. Dies ist eine große Sache für das Caching von Abfrageplänen auf SQL Server und PostgreSQL.## Massenoperationen – ExecuteUpdate und ExecuteDelete

ExecuteUpdate und ExecuteDelete wurden in EF Core 7 eingeführt, aber EF Core 9 erweitert die Möglichkeiten, die Sie mit ihnen machen können, auf sinnvolle Weise.

Komplexere Aktualisierungsausdrücke

Sie können jetzt komplexere Ausdrücke in ExecuteUpdate verwenden, einschließlich Verweisen auf andere Tabellen über Navigationseigenschaften:

await context.Products
    .Where(p => p.Category.IsDiscontinued)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsAvailable, false)
        .SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
        .SetProperty(p => p.Price, p => p.Price * 0.5m));

Dadurch wird eine einzelne UPDATE-Anweisung mit einem JOIN für die Kategorientabelle generiert – keine Notwendigkeit, Entitäten in den Speicher zu laden, kein Aufwand für die Änderungsverfolgung.

Bedingte Massenlöschungen mit Unterabfragen

Massenlöschungen mit Unterabfragefiltern werden jetzt vollständig unterstützt:

await context.AuditLogs
    .Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
    .Where(log => !context.ProtectedRecords
        .Any(pr => pr.AuditLogId == log.Id))
    .ExecuteDeleteAsync();

Dies entspricht einer DELETE-Unterabfrage mit einer NOT EXISTS-Unterabfrage, genau das, was Sie von Hand schreiben würden. Keine Entitäten geladen, keine Hin- und Rückfahrten.

JSON-Spaltenverbesserungen

JSON-Spalten waren eine der aufregendsten Funktionen in den letzten EF Core-Versionen, und EF Core 9 geht noch einen Schritt weiter.

Abfragen innerhalb von JSON

Sie können jetzt Daten aus JSON-Spalten heraus filtern und projizieren, mit besserer Übersetzungsunterstützung:

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public ShippingAddress Address { get; set; } // Stored as JSON
    public List<OrderNote> Notes { get; set; }   // Stored as JSON
}

public class ShippingAddress
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public class OrderNote
{
    public DateTime CreatedAt { get; set; }
    public string Text { get; set; }
    public string Author { get; set; }
}

Konfiguration in Ihrem DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>(builder =>
    {
        builder.OwnsOne(o => o.Address, ab =>
        {
            ab.ToJson();
        });

        builder.OwnsMany(o => o.Notes, nb =>
        {
            nb.ToJson();
        });
    });
}

Jetzt können Sie direkt in JSON abfragen:

var newYorkOrders = await context.Orders
    .Where(o => o.Address.State == "NY")
    .OrderByDescending(o => o.Notes.Count)
    .Select(o => new
    {
        o.CustomerName,
        o.Address.City,
        LatestNote = o.Notes
            .OrderByDescending(n => n.CreatedAt)
            .Select(n => n.Text)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 generiert ordnungsgemäße JSON_VALUE- und JSON_QUERY-Aufrufe auf SQL Server (oder gleichwertig auf anderen Anbietern), und die Übersetzung deckt einen viel größeren Bereich von LINQ-Operationen für JSON-Elemente ab als zuvor.

JSON-Eigenschaften aktualisieren

Einer der Reibungspunkte in EF Core 8 bestand darin, dass das Aktualisieren einer einzelnen Eigenschaft innerhalb einer JSON-Spalte dazu führen würde, dass das gesamte JSON-Dokument neu geschrieben wird. EF Core 9 verbessert dies durch eine detailliertere Änderungsverfolgung für JSON-zugeordnete Typen und generiert nach Möglichkeit gezieltere Updates.

var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();

Bei unterstützten Anbietern kann dies zu einer gezielteren JSON-Änderung führen, anstatt den gesamten Blob neu zu schreiben.

Komplexe Typen – Wertobjekte ohne Identität

Komplexe Typen sind eine der Funktionen, auf die Domain-Driven-Design-Praktiker gewartet haben. Im Gegensatz zu eigenen Typen haben komplexe Typen keine Identität – sie sind reine Wertobjekte.

[ComplexType]
public record Money(decimal Amount, string Currency);

[ComplexType]
public record DateRange(DateTime Start, DateTime End);

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Money Budget { get; set; }
    public DateRange Timeline { get; set; }
}

Diese werden als abgeflachte Spalten in der übergeordneten Tabelle gespeichert – Budget_Amount, Budget_Currency, Timeline_Start, Timeline_End – ohne dass eine separate Tabelle oder ein Schlüssel erforderlich ist.

Der Hauptunterschied zu eigenen Typen: Komplexe Typen werden nach Wert und nicht nach Referenz verglichen. Zwei Money-Instanzen mit demselben Amount und Currency gelten als gleich, unabhängig davon, zu welcher Entität sie gehören.

var expensiveProjects = await context.Projects
    .Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
    .OrderByDescending(p => p.Budget.Amount)
    .ToListAsync();

Dies führt direkt zur Filterung der abgeflachten Spalten – sauber, effizient und genau das, was Sie erwarten.

HierarchyId-Unterstützung für SQL Server

Wenn Sie jemals mit hierarchischen Daten in SQL Server gearbeitet haben – Organigramme, Kategoriebäume, Dateisysteme – wissen Sie, dass HierarchyId der integrierte Typ dafür ist. EF Core 9 bringt dafür erstklassigen Support.

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public HierarchyId PathFromCeo { get; set; }
}

Sie können hierarchische Beziehungen jetzt direkt abfragen:

var managerId = HierarchyId.Parse("/1/3/");

// All direct and indirect reports
var allReports = await context.Employees
    .Where(e => e.PathFromCeo.IsDescendantOf(managerId))
    .Where(e => e.PathFromCeo != managerId) // exclude the manager
    .OrderBy(e => e.PathFromCeo)
    .ToListAsync();

// Direct reports only (one level down)
var directReports = await context.Employees
    .Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
    .ToListAsync();

// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
    .Select(e => new
    {
        e.Name,
        e.Title,
        Level = e.PathFromCeo.GetLevel()
    })
    .OrderBy(e => e.Level)
    .ToListAsync();
```All dies lässt sich in die nativen `HierarchyId`-Methoden von SQL Server übersetzen. Wenn Sie Baumstrukturen mit selbstreferenzierenden Fremdschlüsseln und rekursiven CTEs implementiert haben, ist dies ein viel saubererer Ansatz.

## Kompilierte Modelle und AOT-Unterstützung

Leistungsbewusste Entwickler werden die kontinuierliche Investition in kompilierte Modelle und die Unterstützung der AOT-Kompilierung (Ahead-of-Time) zu schätzen wissen.

### Kompilierte Modelle

Kompilierte Modelle generieren vorab die Modellmetadaten, die EF Core normalerweise beim Start erstellt. Bei großen Modellen (denken Sie an Hunderte von Entitäten) kann dies die Kaltstartzeit erheblich verkürzen.

```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels

Dann verkabeln Sie es:

builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(connectionString)
        .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));

In EF Core 9 sind kompilierte Modelle vollständiger – sie unterstützen mehr Zuordnungsfunktionen und erzeugen eine kleinere Ausgabe. Bei einem Modell mit etwa 400 Entitäten kann die Startzeit von mehreren Sekunden auf nahezu augenblicklich sinken.

AOT-Kompilierungsfortschritt

Die vollständige native AOT-Unterstützung für EF Core ist noch in Arbeit, aber EF Core 9 macht erhebliche Fortschritte. Viele der reflexionsintensiven Codepfade wurden umgestaltet, um das Trimmen zu erleichtern, und kompilierte Modelle sind ein wichtiger Teil der AOT-Geschichte. Wenn Sie auf Szenarios wie Azure Functions oder Microservices abzielen, bei denen ein Kaltstart wichtig ist, sind diese Verbesserungen direkt relevant.

Aktualisierungen des Cosmos DB-Anbieters

Der Azure Cosmos DB-Anbieter wird mit EF Core 9 weiter ausgereift. Einige bemerkenswerte Verbesserungen:

Umgang mit Partitionsschlüsseln

Der Anbieter unterstützt jetzt hierarchische Partitionsschlüssel und handhabt Partitionsschlüsselfilter intelligenter:

public class TenantDocument
{
    public string Id { get; set; }
    public string TenantId { get; set; }  // Partition key
    public string Region { get; set; }     // Sub-partition key
    public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
    .Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
    .Where(d => d.Content.Contains("important"))
    .ToListAsync();

Verbesserte LINQ-zu-NoSQL-Übersetzung

Weitere LINQ-Operationen werden jetzt in den SQL-Dialekt von Cosmos DB übersetzt, einschließlich besserer Unterstützung für Contains, Any, Operationen für verschachtelte Arrays und mathematische Funktionen. Anfragen, die zuvor auf die Client-Auswertung zurückfielen, werden jetzt serverseitig bearbeitet.

Unterstützung für die Vektorsuche

EF Core 9 führt eine frühe Unterstützung für die Vektorähnlichkeitssuche mit Cosmos DB ein, was nützlich ist, wenn Sie Anwendungen erstellen, die Einbettungen oder KI-gesteuerte Suche integrieren:

var results = await context.Documents
    .OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
    .Take(10)
    .ToListAsync();

Migrationsverbesserungen

Die Migrationen haben einige Verbesserungen der Lebensqualität mit sich gebracht, die die Arbeit mit ihnen in Teamumgebungen weniger mühsam machen.

Temporale Tabellen in Migrationen

Migrationen handhaben die Konfiguration temporaler Tabellen jetzt reibungsloser, mit ordnungsgemäßer Unterstützung für Periodenspalten und Verlaufstabellenbenennung:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .ToTable("Employees", b => b.IsTemporal(t =>
        {
            t.HasPeriodStart("ValidFrom");
            t.HasPeriodEnd("ValidTo");
            t.UseHistoryTable("EmployeeHistory");
        }));
}

Idempotente Skripte

Der Befehl Script-Migration (und sein CLI-Äquivalent) erzeugt standardmäßig bessere idempotente Skripte mit verbesserter Handhabung von Randfällen rund um Schemaänderungen, die von in bestimmten Zuständen vorhandenen Daten abhängen.

Migrationspakete

Migrationspakete, die Ihre Migrationen für die Bereitstellung in eine eigenständige ausführbare Datei packen, sind in EF Core 9 zuverlässiger und bieten eine bessere Fehlerberichterstattung und Wiederholungslogik für vorübergehende Fehler.

dotnet ef migrations bundle --self-contained -r linux-x64

Dadurch wird eine Binärdatei erstellt, die Sie in Ihrer CI/CD-Pipeline ausführen können, ohne dass das .NET SDK auf Ihrem Bereitstellungsziel installiert werden muss.

LeistungsbenchmarksHier sind einige grobe Benchmarks aus meinen eigenen Tests. Diese stammen aus einem Projekt mit etwa 200 Entitäten, die mit SQL Server 2022 ausgeführt werden, gemessen mit BenchmarkDotNet. Ihre Zahlen werden variieren, aber die relativen Verbesserungen sollten ähnlich sein.

SzenarioEF Core 8EF Core 9Verbesserung
Modellbau (Kaltstart)1.850 ms320 ms~5,8x schneller (kompiliert)
Einfache Abfrage (einzelne Entität nach PK)0,42 ms0,38 ms~10 % schneller
Komplexe Abfrage (Joins + Aggregation)3,1 ms2,4 ms~23 % schneller
Massenaktualisierung (10.000 Zeilen)145 ms118 ms~19 % schneller
JSON-Spaltenabfrage2,8 ms1,9 ms~32 % schneller
SaveChanges (100 Entitäten)48 ms41 ms~15 % schneller

Die Verbesserung des kompilierten Modells ist die dramatischste, aber die stetigen Verbesserungen auf ganzer Linie summieren sich – insbesondere in Szenarien mit hohem Durchsatz, in denen Sie Tausende von Abfragen pro Sekunde ausführen.

Upgrade von EF Core 8

Wenn Sie EF Core 8 verwenden, ist der Upgrade-Pfad relativ reibungslos. Hier ist eine Checkliste:

1. Aktualisieren Sie Ihre Pakete:

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />

2. Suchen Sie nach wichtigen Änderungen. Die Liste für EF Core 9 ist im Vergleich zu einigen früheren Versionen relativ kurz. Die bemerkenswertesten:

– Einige zuvor veraltete APIs wurden entfernt – Änderungen in der Art und Weise, wie bestimmte GroupBy-Abfragen übersetzt werden (sie gehen jetzt serverseitig, was das Verhalten ändert, wenn Sie sich auf die Client-Auswertung verlassen haben) – Kleinere Änderungen in der Ausgabe des Migrationsgerüsts

3. Generieren Sie kompilierte Modelle neu, wenn Sie sie verwenden. Das Format hat sich geändert, sodass alte kompilierte Modelle nicht mit EF Core 9 funktionieren.

4. Führen Sie Ihre Testsuite aus. Achten Sie besonders auf Abfragen, die zuvor auf dem Client ausgewertet wurden – sie werden jetzt möglicherweise auf dem Server ausgewertet, was normalerweise besser ist, aber zu Datenunterschieden führen kann.

5. Überprüfen Sie Ihre Cosmos DB-Abfragen, wenn Sie diesen Anbieter verwenden. Die verbesserten Übersetzungen bedeuten, dass einige Abfragen anders ausgeführt werden (normalerweise schneller), es lohnt sich jedoch zu überprüfen, ob die Ergebnisse identisch sind.

Ein minimales Upgrade für ein typisches Projekt sieht so aus:

dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test

Wenn alles kompiliert und die Tests bestanden werden, sind Sie wahrscheinlich in guter Verfassung. Wenn Sie auf Probleme stoßen, finden Sie in der Dokumentation zu EF Core 9 Breaking Changes detaillierte Migrationsanleitungen für jede Änderung.

Zusammenfassung

EF Core 9 ist keine revolutionäre Version – es ist eine evolutionäre Version, und genau das musste es sein. Allein die LINQ-Verbesserungen rechtfertigen das Upgrade für die meisten Projekte, und Funktionen wie JSON-Spaltenverbesserungen, komplexe Typen und HierarchyId unterstützen Öffnungsmuster, die zuvor umständlich oder unmöglich waren.

Wenn ich die drei Funktionen auswählen müsste, die den größten Einfluss auf meine Projekte hatten:

  1. Parametrisierte primitive Sammlungen – weil die Effizienz des Abfrageplan-Cache im großen Maßstab von Bedeutung ist
  2. JSON-Spaltenverbesserungen – weil das hybride relationale Dokumentmuster unglaublich nützlich ist
  3. Kompilierte Modelle – weil sich die Startzeit direkt auf die Produktivität der Entwickler und die Bereitstellungsgeschwindigkeit auswirktDas EF Core-Team befindet sich seit EF Core 5 auf einem soliden Weg, und Version 9 setzt diesen Trend fort. Wenn Sie bereits EF Core 8 verwenden, ist das Upgrade risikoarm und lohnend. Wenn Sie etwas Älteres haben, gab es noch nie einen besseren Zeitpunkt, den Sprung zu wagen.

Viel Spaß beim Codieren – und viel Spaß beim Abfragen.