Neuerungen in EF Core 9

EF Core 9 (EF9) ist die nächste Version nach EF Core 8, und das Release ist im November 2024 geplant.

EF9 ist als tägliche Builds verfügbar, die alle neuesten EF9-Features und API-Optimierungen enthalten. Die Beispiele hier nutzen diese täglichen Builds.

Tipp

Sie können alle Beispiele ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen. Jeder Abschnitt unten ist mit dem zugehörigen Quellcode verlinkt.

EF9 ist auf .NET 8 ausgerichtet und kann daher entweder mit .NET 8 (LTS) oder .NET 9 Preview verwendet werden.

Tipp

Die Dokumente zu den Neuerungen werden für jede Vorschauversion aktualisiert. Alle Beispiele sind für die Verwendung der täglichen EF9-Builds ausgelegt, die im Vergleich zur letzten Vorschauversion in der Regel mehrere zusätzliche Wochen an abgeschlossener Arbeit aufweisen. Wir empfehlen dringend, beim Testen neuer Features die täglichen Builds zu verwenden, damit Sie Ihre Tests nicht mit veralteten Elementen durchführen.

Azure Cosmos DB for NoSQL

EF 9.0 verbessert den EF Core-Anbieter für Azure Cosmos DB erheblich. Wichtige Teile des Anbieters wurden umgeschrieben, um neue Funktionen bereitzustellen, neue Formen von Abfragen zu ermöglichen und den Anbieter besser auf die bewährten Methoden von Azure Cosmos DB abzustimmen. Die wichtigsten Verbesserungen auf hoher Ebene sind nachfolgend aufgeführt; eine vollständige Liste finden Sie in diesem epischen Problem.

Warnung

Im Rahmen der Verbesserungen für den Anbieter mussten eine Reihe von wichtigen Änderungen vorgenommen werden; wenn Sie eine bestehende Anwendung aktualisieren, lesen Sie bitte den Abschnitt über die Änderungen sorgfältig durch.

Verbesserungen beim Abfragen mit Partitionsschlüsseln und Dokument-IDs

Jedes Dokument, das in einer Azure Cosmos DB-Datenbank gespeichert ist, verfügt über eine eindeutige Ressourcen-ID. Darüber hinaus kann jedes Dokument einen „Partitionsschlüssel“ enthalten, der die logische Partitionierung der Daten bestimmt, damit die Datenbank effektiv skaliert werden kann. Weitere Informationen zur Auswahl von Partitionsschlüsseln finden Sie in Partitionierung und horizontale Skalierung in Azure Cosmos DB.

In EF 9.0 ist der Azure Cosmos DB-Anbieter wesentlich besser darin, Partitionsschlüsselvergleiche in Ihren LINQ-Abfragen zu identifizieren und sie zu extrahieren, damit Ihre Abfragen nur an die relevante Partition gesendet werden. Dies kann die Leistung Ihrer Abfragen erheblich verbessern und RU-Gebühren reduzieren. Zum Beispiel:

var sessions = await context.Sessions
    .Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
    .ToListAsync();

In dieser Abfrage erkennt der Anbieter automatisch den Vergleich PartitionKey; wenn wir die Protokolle untersuchen, sehen wir Folgendes:

Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")

Beachten Sie, dass die WHERE Klausel nicht enthält PartitionKey: Dieser Vergleich wurde „aufgehoben” und wird verwendet, um die Abfrage nur für die relevante Partition auszuführen. In früheren Versionen wurde der Vergleich in vielen Situationen in der WHERE Klausel belassen, was dazu führte, dass die Abfrage für alle Partitionen ausgeführt wurde, was zu erhöhten Kosten und geringerer Leistung führte.

Wenn Ihre Abfrage außerdem einen Wert für die ID-Eigenschaft des Dokuments bereitstellt und keine anderen Abfragevorgänge enthält, kann der Anbieter eine zusätzliche Optimierung anwenden:

var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
    .Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
    .SingleAsync();

Die Protokolle zeigen Folgendes für diese Abfrage:

Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'

Hier wird überhaupt keine SQL-Abfrage gesendet. Stattdessen führt der Anbieter einen äußerst effizienten Punktlesevorgang (ReadItem API) durch, das das Dokument direkt mit dem Partitionsschlüssel und der ID abruft. Dies ist die effizienteste und kostengünstige Art von Lesevorgängen, die Sie in Azure Cosmos DB ausführen können. Weitere Informationen zu Punktlesevorgängen finden Sie in der Dokumentation zu Azure Cosmos DB.

Weitere Informationen zum Abfragen mit Partitionsschlüsseln und Punktlesevorgängen finden Sie auf der Dokumentationsseite zur Abfrage.

Hierarchische Partitionsschlüssel

Tipp

Der hier gezeigte Code stammt aus HierarchicalPartitionKeysSample.cs.

Azure Cosmos DB hat ursprünglich einen einzelnen Partitionsschlüssel unterstützt, hat aber seitdem die Partitionierungsfunktionen erweitert, um auch die Unterpartitionierung durch Angabe von bis zu drei Hierarchieebenen im Partitionsschlüssel zu unterstützen. EF Core 9 bietet vollständige Unterstützung für hierarchische Partitionsschlüssel, sodass Sie die Leistungs- und Kosteneinsparungen nutzen können, die mit diesem Feature verbunden sind.

Partitionsschlüssel werden mithilfe der Modellerstellungs-API angegeben, in der Regel in DbContext.OnModelCreating. Für jede Ebene des Partitionsschlüssels muss im Entitätstyp eine zugeordnete Eigenschaft vorhanden sein. Ein Beispiel dafür ist folgender UserSession-Entitätstyp:

public class UserSession
{
    // Item ID
    public Guid Id { get; set; }

    // Partition Key
    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }

    // Other members
    public string Username { get; set; } = null!;
}

Der folgende Code gibt einen Partitionsschlüssel mit drei Ebenen mithilfe der Eigenschaften TenantId, UserId und SessionId an:

modelBuilder
    .Entity<UserSession>()
    .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });

Tipp

Diese Partitionsschlüsseldefinition folgt dem Beispiel in Auswählen der hierarchischen Partitionsschlüssel aus der Azure Cosmos DB-Dokumentation.

Beachten Sie, dass ab EF Core 9 die Eigenschaften eines beliebigen zugeordneten Typs im Partitionsschlüssel verwendet werden können. Bei bool und numerischen Typen wie der int SessionId-Eigenschaft wird der Wert direkt im Partitionsschlüssel verwendet. Andere Typen wie die Guid UserId-Eigenschaft werden automatisch in Zeichenfolgen konvertiert.

Bei Abfragen extrahiert EF automatisch die Partitionsschlüsselwerte aus Abfragen und wendet sie auf die Azure Cosmos DB-Abfrage-API an, um sicherzustellen, dass die Abfragen ordnungsgemäß auf die geringstmögliche Anzahl von Partitionen beschränkt werden. Betrachten Sie z. B. die folgende LINQ-Abfrage, die alle drei Partitionsschlüsselwerte in der Hierarchie bereitstellt:

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId == sessionId
             && e.Username.Contains("a"))
    .ToListAsync();

Beim Ausführen dieser Abfrage extrahiert EF Core die Werte der Parameter tenantId, userId und sessionId und übergibt sie als Partitionsschlüsselwert an die Azure Cosmos DB-Abfrage-API. Sehen Sie sich z. B. die Protokolle für die Ausführung der obigen Abfrage an:

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Beachten Sie, dass die Partitionsschlüsselvergleiche aus der WHERE Klausel entfernt wurden und stattdessen als Partitionsschlüssel für eine effiziente Ausführung verwendet werden: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

Weitere Informationen finden Sie in der Dokumentation zum Abfragen mit Partitionsschlüsseln.

Deutlich verbesserte LINQ-Abfragefunktionen

In EF 9.0 wurden die LINQ-Übersetzungsfunktionen des Azure Cosmos DB-Anbieters stark erweitert, und der Anbieter kann nun deutlich mehr Abfragetypen ausführen. Die vollständige Liste der Abfrageverbesserungen ist zu lang, aber hier sind die wichtigsten Highlights:

  • Vollständige Unterstützung der primitiven Sammlungen von EF, sodass Sie LINQ-Abfragen beispielsweise für Sammlungen von ganzen Zahlen oder Zeichenfolgen ausführen können. Weitere Informationen finden Sie unter Was ist neu in EF8: Primitive Sammlungen.
  • Unterstützung beliebiger Abfragen über nicht primitive Auflistungen.
  • Viele zusätzliche LINQ-Operatoren werden jetzt unterstützt: Indizierung in Sammlungen, Length/Count, ElementAt, Containsund viele andere.
  • Unterstützung von Aggregatoperatoren wie Count und Sum.
  • Zusätzliche Funktionsübersetzungen (siehe Dokumentation zu Funktionszuordnungen für die vollständige Liste der unterstützten Übersetzungen):
    • Übersetzungen für Komponentenmember DateTime und DateTimeOffset (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined und EF.Functions.CoalesceUndefined lassen jetzt den Umgang mit undefined Werten zu.
    • string.Contains, StartsWith und EndsWith unterstützt jetzt StringComparison.OrdinalIgnoreCase.

Die vollständige Liste der Abfrageverbesserungen finden Sie in diesem Problem:

Verbesserte Modellierung, die an Azure Cosmos DB- und JSON-Standards ausgerichtet ist

EF 9.0 ordnet Azure Cosmos DB-Dokumenten auf natürliche Arten für eine JSON-basierte Dokumentdatenbank zu und unterstützt die Zusammenarbeit mit anderen Systemen, die auf Ihre Dokumente zugreifen. Obwohl dies zu unterbrechungsfreien Änderungen führt, sind APIs vorhanden, die das Zurücksetzen auf das Verhalten vor 9.0 in allen Fällen ermöglichen.

Vereinfachte id Eigenschaften ohne Diskriminatoren

Zuerst haben frühere Versionen von EF den Diskriminatorwert in die JSON-Eigenschaft id eingefügt, wodurch Dokumente wie die folgenden erstellt werden:

{
    "id": "Blog|1099",
    ...
}

Dies wurde durchgeführt, damit Dokumente unterschiedlicher Typen (z. B. Blog und Post) und derselbe Schlüsselwert (1099) innerhalb derselben Containerpartition vorhanden sein können. Ab EF 9.0 enthält die id Eigenschaft nur den Schlüsselwert:

{
    "id": 1099,
    ...
}

Dies ist ein besserer Weg, um JSON zuzuordnen und die Interaktion mit EF-generierten JSON-Dokumenten zu erleichtern. Solche externen Systeme kennen die EF-Diskriminatorwerte nicht allgemein, die standardmäßig von .NET-Typen abgeleitet sind.

Beachten Sie, dass dies eine wichtige Änderung ist, da EF vorhandene Dokumente nicht mehr mit dem alten id Format abfragen kann. Es wurde eine API eingeführt, um auf das vorherige Verhalten zurückgesetzt zu werden. Weitere Details finden Sie in der aktuellen Änderungsnotiz und in der Dokumentation .

Diskriminator-Eigenschaft umbenannt in $type

Die standardmäßige Diskriminatoreigenschaft wurde zuvor Discriminator genannt. EF 9.0 ändert die Standardeinstellung in $type:

{
    "id": 1099,
    "$type": "Blog",
    ...
}

Dies folgt dem neuen Standard für JSON-Polymorphismus und ermöglicht eine bessere Interoperabilität mit anderen Tools. System.Text.Json von .NET unterstützt zum Beispiel auch Polymorphismus, wobei $type der Standardname der Diskriminatoreigenschaft (Docs) verwendet.

Beachten Sie, dass dies eine wichtige Änderung ist, da EF vorhandene Dokumente nicht mehr mit dem alten Namen der Diskriminatoreigenschaft abfragen kann. Ausführliche Informationen zum Wiederherstellen der vorherigen Benennung finden Sie in der aktuellen Änderungsnotiz.

Suche nach Vektorähnlichkeit (Vorschau)

Azure Cosmos DB bietet jetzt Vorschauunterstützung für die Vektorähnlichkeitssuche. Die Vektorsuche ist ein grundlegender Bestandteil einiger Anwendungstypen, darunter KI und semantische Suche. Mit Azure Cosmos DB können Vektoren zusammen mit Ihren restlichen Daten direkt in Ihren Dokumenten gespeichert werden, was bedeutet, dass Sie alle Ihre Abfragen für eine einzelne Datenbank ausführen können. Dies kann Ihre Architektur erheblich vereinfachen und dazu führen, dass Sie ohne zusätzliche dedizierte Vektordatenbanklösung in Ihrem Stapel auskommen. Weitere Informationen zur Azure Cosmos DB-Vektorsuche finden Sie in der Dokumentation.

Nachdem Ihr Azure Cosmos DB-Container ordnungsgemäß eingerichtet wurde, ist die Verwendung der Vektorsuche über EF ganz einfach. Sie müssen lediglich eine Vektoreigenschaft hinzufügen und sie konfigurieren:

public class Blog
{
    ...

    public float[] Vector { get; set; }
}

public class BloggingContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Embeddings)
            .IsVector(DistanceFunction.Cosine, dimensions: 1536);
    }
}

Nachdem dies abgeschlossen ist, verwenden Sie die EF.Functions.VectorDistance() Funktion in LINQ-Abfragen, um die Vektorgleichheitssuche auszuführen:

var blogs = await context.Blogs
    .OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
    .Take(5)
    .ToListAsync();

Weitere Informationen finden Sie in der Dokumentation zur Vektorsuche.

Unterstützung der Paginierung

Der Azure Cosmos DB-Anbieter ermöglicht jetzt das Paginieren von Abfrageergebnissen über Fortsetzungstoken, was wesentlich effizienter und kostengünstiger ist als die herkömmliche Verwendung von Skip und Take:

var firstPage = await context.Posts
    .OrderBy(p => p.Id)
    .ToPageAsync(pageSize: 10, continuationToken: null);

var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
    // Display/send the posts to the user
}

Der neue ToPageAsync Operator gibt ein CosmosPage, das ein Fortsetzungstoken verfügbar macht, das verwendet werden kann, um die Abfrage zu einem späteren Zeitpunkt effizient fortzusetzen, wobei die nächsten 10 Elemente abgerufen werden:

var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);

Weitere Informationen finden Sie im Dokumentationsabschnitt zur Paginierung.

FromSql für sicherere SQL-Abfragen

Der Azure Cosmos DB-Anbieter hat SQL-Abfragen über FromSqlRaw zugelassen. Diese API kann jedoch anfällig für SQL-Injection-Angriffe sein, wenn vom Benutzer bereitgestellte Daten interpoliert oder mit SQL verkettet werden. In EF 9.0 können Sie jetzt die neue FromSql Methode verwenden, die parametrisierte Daten immer als Parameter außerhalb von SQL integriert:

var maxAngle = 8;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
    .ToListAsync();

Weitere Informationen finden Sie im Dokumentationsabschnitt zur Paginierung.

Rollenbasierter Zugriff

Azure Cosmos DB für NoSQL umfasst ein integriertes rollenbasiertes Zugriffssteuerungssystem (RBAC). Dies wird jetzt von EF9 für alle Datenebenenvorgänge unterstützt. Das Azure Cosmos DB SDK unterstützt jedoch RBAC für Vorgänge auf der Verwaltungsebene in Azure Cosmos DB nicht. Verwenden Sie die Azure Management-API anstelle von EnsureCreatedAsync mit RBAC.

Synchrone E/A ist jetzt standardmäßig blockiert.

Azure Cosmos DB for NoSQL unterstützt keine synchronen (blockierenden) APIs aus Anwendungscode. Zuvor hat EF dies maskiert, indem es asynchrone Aufrufe für Sie blockiert hat. Dies fördert jedoch sowohl die synchrone E/A-Verwendung, was eine schlechte Methode ist und kann auch zu Deadlocks führen. Daher wird ab EF 9 eine Ausnahme ausgelöst, wenn der synchrone Zugriff versucht wird. Zum Beispiel:

Synchrone E/A kann vorerst noch verwendet werden, indem die Warnstufe entsprechend konfiguriert wird. Beispiel: In OnConfiguring in Ihrem DbContext Typ:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

Beachten Sie jedoch, dass wir planen, die Synchronisierungsunterstützung in EF 11 vollständig zu entfernen, also beginnen Sie mit dem Update, um asynchrone Methoden wie ToListAsync und SaveChangesAsync so bald wie möglich zu verwenden!

AOT und vorab kompilierte Abfragen

Warnung

NativeAOT und Abfragevorkompilierung sind sehr experimentelle Features und sind noch nicht für die Produktionsverwendung geeignet. Die unten beschriebene Unterstützung sollte als Infrastruktur für das endgültige Feature betrachtet werden, das wahrscheinlich mit EF 10 veröffentlicht wird. Wir empfehlen Ihnen, mit dem aktuellen Support zu experimentieren und Über Ihre Erfahrungen zu berichten, empfehlen jedoch die Bereitstellung von EF NativeAOT-Anwendungen in der Produktion.

EF 9.0 bietet anfängliche, experimentelle Unterstützung für .NET NativeAOT und ermöglicht die Veröffentlichung von vorab kompilierten Anwendungen, die EF für den Zugriff auf Datenbanken nutzen. Zur Unterstützung von LINQ-Abfragen im NativeAOT-Modus basiert EF auf der Abfragevorkompilierung: Dieser Mechanismus identifiziert statisch EF LINQ-Abfragen und generiert C#-Interceptors, die Code zum Ausführen jeder bestimmten Abfrage enthalten. Dies kann die Startzeit Ihrer Anwendung erheblich reduzieren, da die schwere Verarbeitung und Kompilierung Ihrer LINQ-Abfragen in SQL bei jedem Start der Anwendung nicht mehr geschieht. Stattdessen enthält der Interceptor jeder Abfrage das fertige SQL für diese Abfrage sowie optimierten Code zum Materialisieren von Datenbankergebnissen als .NET-Objekte.

Beispiel: Ein Programm mit der folgenden EF-Abfrage:

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

EF generiert einen C#-Interceptor in Ihrem Projekt, der die Abfrageausführung übernimmt. Anstatt die Abfrage zu verarbeiten und bei jedem Start des Programms in SQL zu übersetzen, verfügt der Interceptor über die sql eingebettete SQL-Datei (in diesem Fall für SQL Server), sodass Ihr Programm viel schneller starten kann:

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));

Darüber hinaus enthält derselbe Interceptor Code, um Ihr .NET-Objekt aus Datenbankergebnissen zu materialisieren:

var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

Dies verwendet ein weiteres neues .NET-Feature – unsichere Accessoren, um Daten aus der Datenbank in die privaten Felder Ihres Objekts einzufügen.

Wenn Sie an NativeAOT interessiert sind und gerne mit modernsten Features experimentieren möchten, probieren Sie es aus! Beachten Sie einfach, dass das Feature als instabil betrachtet werden sollte und derzeit viele Einschränkungen hat; wir erwarten, sie zu stabilisieren und für den Produktionseinsatz in EF 10 besser geeignet zu machen.

Weitere Details finden Sie auf der Dokumentationsseite von NativeAOT.

LINQ- und SQL-Übersetzung

Wie bei jeder Version enthält EF9 eine große Anzahl von Verbesserungen an den LINQ-Abfragefunktionen. Neue Abfragen können übersetzt werden, und viele SQL-Übersetzungen für unterstützte Szenarien wurden verbessert, um sowohl die Leistung als auch die Lesbarkeit zu verbessern.

Die Anzahl der Verbesserungen ist zu groß, um sie hier aufzulisten. Im Folgenden werden einige der wichtigeren Verbesserungen hervorgehoben; sehen Sie sich dieses Problem an, um eine vollständigere Auflistung der in 9.0 durchgeführten Arbeiten zu finden.

Wir möchten Andrea Canciani (@ranma42) für seine zahlreichen qualitativ hochwertigen Beiträge zur Optimierung der SQL hervorheben, die von EF Core generiert wird!

Komplexe Typen: GroupBy- und ExecuteUpdate-Unterstützung

GroupBy

Tipp

Der hier gezeigte Code stammt aus ComplexTypesSample.cs.

EF9 unterstützt die Gruppierung nach einer komplexen Typinstanz. Zum Beispiel:

var groupedAddresses = await context.Stores
    .GroupBy(b => b.StoreAddress)
    .Select(g => new { g.Key, Count = g.Count() })
    .ToListAsync();

EF übersetzt dies als Gruppierung nach jedem Element des komplexen Typs, der sich an der Semantik komplexer Typen als Wertobjekte richtet. Beispiel in Azure SQL:

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

ExecuteUpdate

Tipp

Der hier gezeigte Code stammt aus ExecuteUpdateSample.cs.

Ebenso wurde in EF9 ExecuteUpdate verbessert, um komplexe Typeigenschaften zu akzeptieren. Jedes Element des komplexen Typs muss jedoch explizit angegeben werden. Zum Beispiel:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

Dadurch wird SQL generiert, die jede Spalte aktualisiert, die dem komplexen Typ zugeordnet ist:

UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
    [s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
    [s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

Zuvor mussten Sie die verschiedenen Eigenschaften des komplexen Typs in Ihrem ExecuteUpdate Aufruf manuell auflisten.

Nicht benötigte Elemente aus SQL bereinigen

Früher produzierte EF manchmal SQL, die Elemente enthielten, die eigentlich nicht benötigt wurden; in den meisten Fällen waren diese möglicherweise in einer früheren Phase der SQL-Verarbeitung erforderlich und wurden zurückgelassen. EF9 löscht nun die meisten dieser Elemente, was zu einer kompakteren und in einigen Fällen effizienteren SQL führt.

Tabellenbereinigung

Als erstes Beispiel enthielt die von EF generierte SQL manchmal JOINs in Tabellen, die in der Abfrage nicht tatsächlich benötigt wurden. Betrachten Sie das folgende Modell, das die Vererbungszuordnung für Tabellen pro Typ (TPT) verwendet:

public class Order
{
    public int Id { get; set; }
    ...

    public Customer Customer { get; set; }
}

public class DiscountedOrder : Order
{
    public double Discount { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    ...

    public List<Order> Orders { get; set; }
}

public class BlogContext : DbContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().UseTptMappingStrategy();
    }
}

Wenn wir dann die folgende Abfrage ausführen, um alle Kunden mit mindestens einer Bestellung abzurufen:

var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();

EF8 generierte das folgende SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
    WHERE [c].[Id] = [o].[CustomerId])

Beachten Sie, dass die Abfrage eine Verknüpfung mit der DiscountedOrders Tabelle enthielt, obwohl darauf keine Spalten verwiesen wurden. EF9 generiert eine bereinigte SQL ohne Verknüpfung:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId])

Projektionsbereinigung

In ähnlicher Weise untersuchen wir die folgende Abfrage:

var orders = await context.Orders
    .Where(o => o.Amount > 10)
    .Take(5)
    .CountAsync();

In EF8 hat diese Abfrage die folgende SQL generiert:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) [o].[Id]
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [t]

Beachten Sie, dass die [o].[Id] Projektion in der Unterabfrage nicht benötigt wird, da der äußere SELECT-Ausdruck einfach die Zeilen zählt. EF9 generiert stattdessen Folgendes:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) 1 AS empty
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [s]

... und die Projektion ist leer. Dies scheint vielleicht nicht viel zu sein, aber es kann die SQL in einigen Fällen erheblich vereinfachen; Sie können gerne durch einige der SQL-Änderungen in den Tests scrollen, um den Effekt anzuzeigen.

Übersetzungen mit GREATEST/LEAST

Tipp

Der hier gezeigte Code stammt aus LeastGreatestSample.cs.

Es wurden mehrere neue Übersetzungen eingeführt, die die SQL-Funktionen GREATEST und LEAST verwenden.

Wichtig

Die Funktionen GREATEST und LEAST wurden in SQL Server-/Azure SQL-Datenbanken in der Version 2022 eingeführt. Visual Studio 2022 installiert SQL Server 2019 standardmäßig. Es wird empfohlen, SQL Server Developer Edition 2022 zu installieren, um diese neuen Übersetzungen in EF9 auszuprobieren.

Abfragen mit Math.Max oder Math.Min werden jetzt beispielsweise mit GREATEST bzw. LEAST für Azure SQL übersetzt. Beispiel:

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

Diese Abfrage wird in folgenden SQL-Code übersetzt, wenn EF9 für SQL Server 2022 ausgeführt wird:

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([w].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b])) >

Math.Min und Math.Max können auch für die Werte einer primitiven Collection verwendet werden. Beispiel:

var pubsInlineMax = await context.Pubs
    .SelectMany(e => e.Counts)
    .Where(e => Math.Max(e, threshold) > top)
    .ToListAsync();

Diese Abfrage wird in folgenden SQL-Code übersetzt, wenn EF9 für SQL Server 2022 ausgeführt wird:

SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1

Schließlich können RelationalDbFunctionsExtensions.Least und RelationalDbFunctionsExtensions.Greatest verwendet werden, um die Least- oder Greatest-Funktion in SQL direkt aufzurufen. Beispiel:

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

Diese Abfrage wird in folgenden SQL-Code übersetzt, wenn EF9 für SQL Server 2022 ausgeführt wird:

SELECT LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([p].[Counts]) AS [c]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]

Erzwingen oder Verhindern der Abfrageparameterisierung

Tipp

Der hier gezeigte Code stammt aus QuerySample.cs.

Außer in einigen Sonderfällen parametrisiert EF Core Variablen, die in einer LINQ-Abfrage verwendet werden, fügt jedoch Konstanten in den generierten SQL-Code ein. Nehmen Sie die folgende Abfragemethode als Beispiel:

async Task<List<Post>> GetPosts(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == id)
        .ToListAsync();

Diese wird bei Verwendung von Azure SQL in die folgenden SQL- und Parameter übersetzt:

Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

Sie sehen, dass EF eine Konstante im SQL-Code für „.NET Blog“ erstellt hat, da dieser Wert sich nicht von Abfrage zu Abfrage ändert. Durch die Verwendung einer Konstanten kann dieser Wert von der Datenbank-Engine untersucht werden, wenn ein Abfrageplan erstellt wird, wodurch Abfragen effizienter werden können.

Andererseits wird der Wert von id parametrisiert, da dieselbe Abfrage mit vielen verschiedenen Werten für id ausgeführt werden kann. Das Erstellen einer Konstante würde in diesem Fall zu einer Verschmutzung des Abfragecaches mit vielen Abfragen führen, die sich nur in id den Werten unterscheiden. Das ist für die Gesamtleistung der Datenbank sehr schlecht.

Im Allgemeinen sollten diese Standardwerte nicht geändert werden. In EF Core 8.0.2 wurde jedoch eine EF.Constant-Methode eingeführt, die erzwingt, dass EF eine Konstante verwendet, auch wenn standardmäßig ein Parameter verwendet werden würde. Beispiel:

async Task<List<Post>> GetPostsForceConstant(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
        .ToListAsync();

Die Übersetzung enthält nun eine Konstante für den id-Wert:

Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

Die EF.Parameter -Methode

In EF9 wird die EF.Parameter-Methode eingeführt, um das Gegenteil zu erreichen. Es wird also erzwungen, dass EF einen Parameter verwendet, auch wenn der Wert im Code eine Konstante ist. Beispiel:

async Task<List<Post>> GetPostsForceParameter(int id)
    => await context.Posts
        .Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
        .ToListAsync();

Die Übersetzung enthält jetzt einen Parameter für die Zeichenfolge „.NET Blog“:

Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

Parametrisierte primitive Sammlungen

EF8 hat die Übersetzung einiger Abfragen, die primitive Sammlungen verwenden, verändert. Wenn eine LINQ-Abfrage eine parametrisierte primitive Sammlung enthält, konvertiert EF den Inhalt in JSON und übergibt ihn als einzelnen Parameterwert der Abfrage:

async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
        .ToListAsync();

Dies führt zur folgenden Übersetzung für SQL Server:

Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

Dadurch kann die gleiche SQL-Abfrage für verschiedene parametrisierte Sammlungen (nur der Parameterwert ändert sich) vorhanden sein. In einigen Fällen kann dies jedoch zu Leistungsproblemen führen, da die Datenbank nicht optimal für die Abfrage planen kann. Die Methode EF.Constant kann zum Wiederherstellen der vorherigen Übersetzung verwendet werden.

In der folgenden Abfrage wird EF.Constant verwendet, um das zu erreichen:

async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
        .ToListAsync();

Der resultierende SQL-Code sieht wie folgt aus:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)

Darüber hinaus führt EF9 die TranslateParameterizedCollectionsToConstantsKontextoption ein, die verwendet werden kann, um die Parametrisierung der primitiven Sammlung für alle Abfragen zu verhindern. Außerdem wurde die komplementäre Option TranslateParameterizedCollectionsToParameters hinzugefügt, die die Parametrisierung primitiver Sammlungen explizit erzwingt, was dem Standardverhalten entspricht.

Tipp

Die Methode EF.Parameter setzt die Kontextoption außer Kraft. Wenn Sie die Parametrisierung primitiver Sammlungen für die meisten (aber nicht für alle) Abfragen verhindern möchten, können Sie die Kontextoption TranslateParameterizedCollectionsToConstants festlegen und EF.Parameter für die Abfragen oder einzelnen Variablen verwenden, die Sie parametrisieren möchten.

Nicht korrelierte Inline-Unterabfragen

Tipp

Der hier gezeigte Code stammt aus QuerySample.cs.

In EF8 kann ein IQueryable-Verweis in einer anderen Abfrage als separater Datenbank-Roundtrip ausgeführt werden. Betrachten Sie zum Beispiel die folgende LINQ-Abfrage:

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

In EF8 wird die Abfrage für dotnetPosts als ein Roundtrip ausgeführt, und dann werden die endgültigen Ergebnisse als zweite Abfrage ausgeführt. Beispielsweise unter SQL Server:

SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

In EF9 ist der IQueryable in der dotnetPosts eingebettet, was zu einem Einzeldatenbank-Roundtrip führt:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
    SELECT COUNT(*)
    FROM [Posts] AS [p0]
    WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

Aggregieren von Funktionen über Unterabfragen und Aggregate in SQL Server

EF9 verbessert die Übersetzung einiger komplexer Abfragen mithilfe von Aggregatfunktionen, die für Unterabfragen oder andere Aggregatfunktionen erstellt wurden. Hier sehen Sie ein Beispiel für eine solche Abfrage:

var latestPostsAverageRatingByLanguage = await context.Blogs
    .Select(x => new
    {
        x.Language,
        LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
    })
    .GroupBy(x => x.Language)
    .Select(x => x.Average(xx => xx.LatestPostRating))
    .ToListAsync();

Zuerst wird LatestPostRating durch Select für jeden Post berechnet. Bei der Übersetzung in SQL ist dafür eine Unterabfrage erforderlich. Später in der Abfrage werden diese Ergebnisse dann mithilfe des Vorgangs Average aggregiert. Der resultierende SQL-Code sieht wie folgt aus, wenn er in SQL Server ausgeführt wird:

SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
    SELECT TOP(1) [p].[Rating]
    FROM [Posts] AS [p]
    WHERE [b].[Id] = [p].[BlogId]
    ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]

In früheren Versionen wurde von EF Core ungültiger SQL-Code für ähnliche Abfragen generiert. Dabei wurde versucht, den Aggregatvorgang direkt über die Unterabfrage anzuwenden. Dies ist für SQL Server nicht zulässig und führt zu einer Ausnahme. Das gleiche Prinzip gilt für Abfragen, bei denen ein Aggregat über ein anderes Aggregat verwendet wird:

var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
    x.Language,
    TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();

Hinweis

Diese Änderung hat keine Auswirkungen auf SQLite. SQLite unterstützt Aggregate über Unterabfragen (oder andere Aggregate), aber nicht LATERAL JOIN (APPLY). Hier sehen Sie den SQL-Code für die erste Abfrage, die in SQLite ausgeführt wird:

SELECT ef_avg((
    SELECT "p"."Rating"
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId"
    ORDER BY "p"."PublishedOn" DESC
    LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"

Abfragen mit „Count != 0“ wurden optimiert

Tipp

Der hier gezeigte Code stammt aus QuerySample.cs.

In EF8 wurde die folgende LINQ-Abfrage übersetzt, um die SQL-Funktion COUNT zu verwenden:

var blogsWithPost = await context.Blogs
    .Where(b => b.Posts.Count > 0)
    .ToListAsync();

EF9 generiert jetzt eine effizientere Übersetzung mit EXISTS:

SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
    SELECT 1
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId")

C#-Semantik für Vergleichsvorgänge für Nullwerte

In EF8 wurden Vergleiche zwischen nullbaren Elementen in einigen Szenarien nicht korrekt durchgeführt. In C# ist das Ergebnis einer Vergleichsoperation „false”, wenn einer oder beide Operanden null sind; andernfalls werden die enthaltenen Werte der Operanden verglichen. In EF8 haben wir Vergleiche mithilfe der Datenbank-NULL-Semantik übersetzt. Dies würde zu anderen Ergebnissen führen als eine ähnliche Abfrage mit LINQ to Objects. Darüber hinaus würden wir unterschiedliche Ergebnisse erzielen, wenn der Vergleich im Filter vs. Projektion durchgeführt wurde. Einige Abfragen würden auch unterschiedliche Ergebnisse zwischen SQL Server und SQLite/Postgres liefern.

Beispielsweise die Abfrage:

var negatedNullableComparisonFilter = await context.Entities
    .Where(x => !(x.NullableIntOne > x.NullableIntTwo))
    .Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();

würde die folgende SQL generieren:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])

die Entitäten herausfiltern, deren NullableIntOne oder NullableIntTwo auf NULL festgelegt sind.

In EF9 produzieren wir:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
    WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
    ELSE CAST(1 AS bit)
END = CAST(1 AS bit)

Ein ähnlicher Vergleich wird in einer Projektion durchgeführt:

var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
    x.NullableIntOne,
    x.NullableIntTwo,
    Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();

führte zu folgendem SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
    WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]

die false für Entitäten zurückgegeben wird, deren NullableIntOne oder NullableIntTwo auf NULL festgelegt sind (und nicht auf true wie in C#erwartet). Ausführen desselben Szenarios für Sqlite generiert:

SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"

dies führt zu Nullable object must have a value einer Ausnahme, da die Übersetzung einen null Wert für Fälle erzeugt, in denen NullableIntOne null ist oder NullableIntTwo null sind.

EF9 behandelt diese Szenarien jetzt ordnungsgemäß und erzeugt Ergebnisse, die mit LINQ to Objects und verschiedenen Anbietern konsistent sind.

Zur Verbesserung trug @ranma42 bei. Danke vielmals!

Übersetzung von LINQ-Operatoren des Typs Order und OrderDescending

EF9 ermöglicht die Übersetzung vereinfachter LINQ-Sortiervorgänge (Order und OrderDescending). Diese funktionieren ähnlich wie OrderBy/OrderByDescending, erfordern aber kein Argument. Stattdessen wenden sie die Standardreihenfolge an. Für Entitäten bedeutet das, dass die Sortierung basierend auf Primärschlüsselwerten erfolgt. Bei anderen Typen basiert die Reihenfolge auf den Werten selbst.

Hier sehen Sie eine Beispielabfrage, bei der die vereinfachten Sortieroperatoren genutzt werden:

var orderOperation = await context.Blogs
    .Order()
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderDescending().ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
    })
    .ToListAsync();

Diese Abfrage entspricht Folgendem:

var orderByEquivalent = await context.Blogs
    .OrderBy(x => x.Id)
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
    })
    .ToListAsync();

Die Abfrage generiert folgenden SQL-Code:

SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]

Hinweis

Die Methoden Order und OrderDescending werden nur für Sammlungen von Entitäten, komplexen Typen oder Skalaren unterstützt. Bei komplexeren Projektionen wie etwa Sammlungen anonymer Typen, die mehrere Eigenschaften enthalten, funktionieren sie nicht.

Diese Verbesserung wurde vom Alumnus @bricelam des EF-Teams beigesteuert. Danke vielmals!

Verbesserte Übersetzung des logischen Negationsoperators (!)

EF9 bringt viele Optimierungen rund um SQL CASE/WHEN, COALESCE, Negation und verschiedene andere Konstrukte. Die meisten davon wurden von Andrea Canciani (@ranma42) beigesteuert. Vielen Dank! Im Folgenden werden nur einige dieser Optimierungen rund um die logische Negation beschrieben.

Schauen wir uns die folgende Abfrage an:

var negatedContainsSimplification = await context.Posts
    .Where(p => !p.Content.Contains("Announcing"))
    .Select(p => new { p.Content }).ToListAsync();

In EF8 würden wir die folgende SQL-Datei erstellen:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)

In EF9 „pushen” wir NOT Vorgänge in den Vergleich:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0

Ein weiteres Beispiel, das für SQL Server gilt, ist ein negierter bedingter Vorgang.

var caseSimplification = await context.Blogs
    .Select(b => !(b.Id > 5 ? false : true))
    .ToListAsync();

In EF8 führte dies früher zu geschachtelten CASE Blöcken:

SELECT CASE
    WHEN CASE
        WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
        ELSE CAST(1 AS bit)
    END = CAST(0 AS bit) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

In EF9 haben wir die Schachtelung entfernt:

SELECT CASE
    WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

Auf SQL Server, wenn eine negierte Bool-Eigenschaft projiziert wird:

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();

EF8 würde einen CASE Block generieren, da Vergleiche in der Projektion nicht direkt in SQL Server-Abfragen angezeigt werden können:

SELECT [p].[Title], CASE
   WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
   ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]

In EF9 wurde diese Übersetzung vereinfacht und verwendet jetzt bitweises NOT (~):

SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]

Bessere Unterstützung von Azure SQL und Azure Synapse

EF9 bietet mehr Flexibilität beim Angeben der Art von SQL Server, die als Ziel verwendet werden soll. Anstatt EF mit UseSqlServer zu konfigurieren, können Sie jetzt UseAzureSql oder UseAzureSynapse angeben. Dadurch kann EF bei Verwendung von Azure SQL oder Azure Synapse besseren SQL-Code generieren. EF kann die datenbankspezifischen Features (z. B. dedizierter Typ für JSON in Azure SQL) nutzen oder die eigenen Einschränkungen umgehen (Beispiel: ESCAPE-Klausel ist bei Verwendung von LIKE in Azure Synapse nicht verfügbar).

Andere Verbesserungen der Abfrage

  • Die in EF8 eingeführte Unterstützung für primitive Auflistungen wurde erweitertICollection<T>, um alle Typen zu unterstützen. Beachten Sie, dass dies nur für Parameter- und Inlineauflistungen gilt – primitive Auflistungen, die Teil von Entitäten sind, sind weiterhin auf Arrays, Listen und in EF9 auch schreibgeschützte Arrays/Listen beschränkt.
  • Neue ToHashSetAsync Funktionen, um die Ergebnisse einer Abfrage als eine HashSet (#30033, von @wertzui beigetragen) zurückzugeben.
  • TimeOnly.FromDateTime und FromTimeSpan werden jetzt in SQL Server übersetzt (#33678).
  • ToString über Enums wird jetzt übersetzt (#33706, von @Danevandy99 beigetragen).
  • string.Join wird nun in CONCAT_WS im nicht-aggregierten Kontext auf SQL Server (#28899) übersetzt.
  • EF.Functions.PatIndex wird nun in die SQL Server-Funktion PATINDEX übersetzt, die die Startposition des ersten Vorkommens eines Musters (#33702, @smnsht) zurückgibt.
  • Sum und Average arbeiten jetzt für Dezimalstellen auf SQLite (#33721, von @ranma42 beigetragen).
  • Korrekturen und Optimierungen an string.StartsWith und EndsWith (#31482).
  • Convert.To* Methoden können jetzt Argument vom Typ object akzeptieren (#33891, von @imangd beigetragen).
  • Der XOR-Vorgang (exklusives OR) wird jetzt in SQL Server übersetzt (#34071, von @ranma42 beigetragen).
  • Optimierungen rund um die NULL-Zulässigkeit für Vorgänge vom Typ COLLATE und AT TIME ZONE (#34263, von @ranma42 beigetragen).
  • Optimierungen für DISTINCT über IN-, EXISTS- und Set-Vorgänge (#34381, von @ranma42 beigetragen).

Die obigen Verbesserungen waren nur einige der wichtigeren Abfrageverbesserungen in EF9; dieses Problem finden Sie in einer vollständigeren Auflistung.

Migrationen

Schutz vor gleichzeitigen Migrationen

EF9 führt einen Sperrmechanismus ein, um zu verhindern, dass mehrere Migrationen gleichzeitig ausgeführt werden, da die Datenbank dadurch beschädigt werden kann. Das passiert nicht, wenn Migrationen mithilfe empfohlener Methoden in der Produktionsumgebung bereitgestellt werden. Es kann jedoch passieren, wenn Migrationen zur Laufzeit mithilfe der DbContext.Database.Migrate()-Methode angewendet werden. Es wird empfohlen, Migrationen bei der Bereitstellung und nicht im Rahmen des Anwendungsstarts anzuwenden. Dies kann jedoch zu komplizierteren Anwendungsarchitekturen führen (z. B. bei Verwendung von .NET Aspire-Projekten).

Hinweis

Sehen Sie sich bei Verwendung einer SQLite-Datenbank die potenziellen Probleme im Zusammenhang mit diesem Feature an.

Warnen, wenn mehrere Migrationsvorgänge nicht innerhalb einer Transaktion ausgeführt werden können

Der Großteil der während der Migration ausgeführten Vorgänge wird durch eine Transaktion geschützt. Dadurch wird sichergestellt, dass die Datenbank nicht beschädigt wird, wenn aus irgendeinem Grund ein Fehler auftritt. Einige Vorgänge werden jedoch nicht in eine Transaktion eingeschlossen (z. B. Vorgänge in speicheroptimierten Tabellen in SQL Server oder Datenbankänderungsvorgänge wie das Ändern der Datenbanksortierung). Um eine Beschädigung der Datenbank im Falle eines Migrationsfehlers zu vermeiden, wird empfohlen, diese Vorgänge mithilfe einer separaten Migration isoliert auszuführen. EF9 erkennt jetzt ein Szenario, bei dem eine Migration mehrere Vorgänge enthält, von denen einer nicht in eine Transaktion eingeschlossen werden kann, und gibt eine Warnung aus.

Verbessertes Datenseeding

In EF9 wurde eine praktische Möglichkeit zum Ausführen von Datenseeding eingeführt, um die Datenbank mit anfänglichen Daten aufzufüllen. DbContextOptionsBuilder enthält jetzt Methoden vom Typ UseSeeding und UseAsyncSeeding, die ausgeführt werden, wenn der Datenbankkontext (DbContext) initialisiert wird (im Rahmen von EnsureCreatedAsync).

Hinweis

Wenn die Anwendung schon einmal ausgeführt wurde, enthält die Datenbank unter Umständen bereits die Beispieldaten (die bei der ersten Initialisierung des Kontexts hinzugefügt wurden). Daher muss von UseSeeding und UseAsyncSeeding geprüft werden, ob Daten vorhanden sind, bevor versucht wird, die Datenbank aufzufüllen. Hierzu kann eine einfache EF-Abfrage verwendet werden.

Im folgenden Beispiel wird gezeigt, wie diese Methoden verwendet werden können:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

Weitere Informationen finden Sie hier.

Weitere Migrationsverbesserungen

  • Beim Ändern einer vorhandenen Tabelle in eine zeitliche SQL Server-Tabelle wurde die Größe des Migrationscodes erheblich reduziert.

Modellerstellung

Automatisch kompilierte Modelle

Tipp

Der hier gezeigte Code stammt aus dem NewInEFCore9.CompiledModels Beispiel.

Kompilierte Modelle können die Startzeit für Anwendungen mit großen Modellen verbessern – d. h. Entitätstypanzahl in den 100er- oder 1000er-Jahren. In früheren Versionen von EF Core musste mithilfe der Befehlszeile ein kompiliertes Modell manuell generiert werden. Zum Beispiel:

dotnet ef dbcontext optimize

Nach dem Ausführen des Befehls muss .UseModel(MyCompiledModels.BlogsContextModel.Instance) OnConfiguring hinzugefügt werden, um EF Core anzuweisen, das kompilierte Modell zu verwenden.

Ab EF9 ist diese .UseModel Zeile nicht mehr erforderlich, wenn sich der DbContext Typ der Anwendung im selben Projekt/derselben Assembly wie das kompilierte Modell befindet. Stattdessen wird das kompilierte Modell erkannt und automatisch verwendet. Dies kann beim Erstellen des Modells durch das EF-Protokoll angezeigt werden. Wenn Sie eine einfache Anwendung ausführen, wird das Erstellen des Modells beim Starten der Anwendung angezeigt:

Starting application...
>> EF is building the model...
Model loaded with 2 entity types.

Die Ausgabe der Ausführung von dotnet ef dbcontext optimize für das Modellprojekt lautet:

PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize

Build succeeded in 0.3s

Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> 

Beachten Sie, dass die Protokollausgabe angibt, dass das Modell erstellt wurde, wenn der Befehl ausgeführt wird. Wenn wir die Anwendung jetzt erneut ausführen, nach der Neuerstellung, aber ohne Codeänderungen, lautet die Ausgabe:

Starting application...
Model loaded with 2 entity types.

Beachten Sie, dass das Modell beim Starten der Anwendung nicht erstellt wurde, da das kompilierte Modell erkannt und automatisch verwendet wurde.

MSBuild-Integration

Mit dem obigen Ansatz muss das kompilierte Modell immer noch manuell neu generiert werden, wenn die Entitätstypen oder DbContext Konfiguration geändert werden. EF9 wird jedoch mit einem MSBuild-Aufgabenpaket ausgeliefert, das das kompilierte Modell automatisch aktualisieren kann, wenn das Modellprojekt erstellt wird! Installieren Sie zunächst das Microsoft.EntityFrameworkCore.Tasks NuGet-Paket. Zum Beispiel:

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0

Tipp

Verwenden Sie die Paketversion im obigen Befehl, die der Version von EF Core entspricht, die Sie verwenden.

Aktivieren Sie dann die Integration, indem Sie die EFOptimizeContext Und-Eigenschaften EFScaffoldModelStage in Ihrer .csproj Datei festlegen. Zum Beispiel:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>

Wenn wir nun das Projekt erstellen, können wir die Protokollierung zur Erstellungszeit sehen, die angibt, dass das kompilierte Modell erstellt wird:

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

Und das Ausführen der Anwendung zeigt, dass das kompilierte Modell erkannt wurde und daher das Modell nicht erneut erstellt wird:

Starting application...
Model loaded with 2 entity types.

Sobald das Modell geändert wird, wird das kompilierte Modell automatisch neu erstellt, sobald das Projekt erstellt wird.

Weitere Informationen finden Sie unter MSBuild-Integration.

Schreibgeschützte einfache Sammlungen

Tipp

Der hier gezeigte Code stammt aus PrimitiveCollectionsSample.cs.

EF8 hat Unterstützung für Zuordnungsarrays und veränderbare Listen von Grundtypen eingeführt. Dies wurde in EF9 um schreibgeschützte Sammlungen/Listen erweitert. Insbesondere unterstützt EF9 Sammlungen, die als IReadOnlyList, IReadOnlyCollection oder ReadOnlyCollectioneingegeben werden. Im folgenden Code wird DaysVisited z. B. gemäß der Konvention als einfache Sammlung von Datumsangaben zugeordnet:

public class DogWalk
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

Die schreibgeschützte Sammlung kann bei Bedarf durch eine normale, änderbare Sammlung gesichert werden. Im folgenden Code können DaysVisited z. B. als einfache Sammlung von Datumsangaben zugeordnet werden, während Code in der Klasse die zugrunde liegende Liste bearbeiten kann.

    public class Pub
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IReadOnlyCollection<string> Beers { get; set; }

        private List<DateOnly> _daysVisited = new();
        public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
    }

Diese Sammlungen können dann auf normale Weise in Abfragen verwendet werden. Nehmen Sie diese LINQ-Abfrage als Beispiel:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

Dies übersetzt sich in die folgende SQL-Datei in SQLite:

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
    SELECT COUNT(*)
    FROM json_each("w"."DaysVisited") AS "d"
    WHERE "d"."value" IN (
        SELECT "d0"."value"
        FROM json_each("p"."DaysVisited") AS "d0"
    )) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

Angeben des Füllfaktors für Schlüssel und Indizes

Tipp

Der hier gezeigte Code stammt aus ModelBuildingSample.cs.

EF9 unterstützt die Angabe des SQL Server-Füllfaktors bei der Verwendung von EF Core-Migrationen zur Erstellung von Schlüsseln und Indizes. In der SQL Server-Dokumentation heißt es: „Wenn ein Index erstellt oder neu erstellt wird, bestimmt der Füllfaktorwert den prozentualen Speicherplatz, der auf jeder Blattebenenseite mit Daten gefüllt werden soll. Der Rest jeder Seite wird als freier Speicherplatz für zukünftige Erweiterungen reserviert.“

Der Füllfaktor kann für einzelne oder zusammengesetzte Primär- und Sekundärschlüssel und Indizes festgelegt werden. Zum Beispiel:

modelBuilder.Entity<User>()
    .HasKey(e => e.Id)
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasAlternateKey(e => new { e.Region, e.Ssn })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Name })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Region, e.Tag })
    .HasFillFactor(80);

Bei Anwendung auf vorhandene Tabellen werden die Tabellen in den Füllfaktor für die Einschränkung geändert:

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);

Diese Verbesserung wurde von @deano-hunter beigesteuert. Danke vielmals!

Erweiterbarkeit vorhandener Modellerstellungskonventionen

Tipp

Der hier gezeigte Code stammt aus CustomConventionsSample.cs.

Öffentliche Modellerstellungskonventionen für Anwendungen wurden in EF7 eingeführt. In EF9 ist es nun einfacher, einige der bestehenden Konventionen zu erweitern. Der Code zum Zuordnen von Eigenschaften nach Attribut lautet in EF7 beispielsweise:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

In EF9 kann er folgendermaßen vereinfacht werden:

public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
    : PropertyDiscoveryConvention(dependencies)
{
    protected override bool IsCandidatePrimitiveProperty(
        MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
    {
        if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                return true;
            }

            structuralType.Builder.Ignore(memberInfo.Name);
        }

        mapping = null;
        return false;
    }
}

Aktualisierung von ApplyConfigurationsFromAssembly, um nicht öffentliche Konstruktoren aufzurufen

In früheren Versionen von EF Core instanziierte die ApplyConfigurationsFromAssembly-Methode nur Konfigurationstypen mit öffentlichen, parameterlosen Konstruktoren. In EF9 wurden sowohl die Fehlermeldungen verbessert, die generiert werden, wenn dieser Fehler auftritt, als auch die Instanziierung durch nicht öffentliche Konstruktoren aktiviert. Das ist nützlich, wenn Sie die Konfiguration in einer privaten geschachtelten Klasse zusammenstellen, die niemals durch Anwendungscode instanziiert werden sollte. Beispiel:

public class Country
{
    public int Code { get; set; }
    public required string Name { get; set; }

    private class FooConfiguration : IEntityTypeConfiguration<Country>
    {
        private FooConfiguration()
        {
        }

        public void Configure(EntityTypeBuilder<Country> builder)
        {
            builder.HasKey(e => e.Code);
        }
    }
}

Manche halten nichts von dem Muster, weil es den Entitätstyp mit der Konfiguration koppelt. Andere finden es nützlich, weil es die Konfiguration dem Entitätstyp zuordnet. Doch lassen Sie uns das nicht hier diskutieren. :-)

HierarchyId von SQL Server

Tipp

Der hier gezeigte Code stammt aus HierarchyIdSample.cs.

Sugar-Methode für HierarchyId-Pfadgenerierung

Die Unterstützung der ersten Klasse für den SQL Server-Typ HierarchyId wurde in EF8 hinzugefügt. In EF9 wurde eine Sugar-Methode hinzugefügt, um die Erstellung neuer untergeordneter Knoten in der Struktur zu vereinfachen. Der folgende Code fragt z. B. nach einer vorhandenen Entität mit einer HierarchyId-Eigenschaft ab:

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");

Diese HierarchyId-Eigenschaft kann dann verwendet werden, um untergeordnete Knoten ohne explizite Zeichenfolgenbearbeitung zu erstellen. Zum Beispiel:

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");

Wenn daisy den HierarchyId-Wert /4/1/3/1/ hat, erhält child1 den HierarchyId-Wert „/4/1/3/1/1/1/“, und child2 erhält denHierarchyId-Wert „/4/1/3/1/1/2/“.

Um einen Knoten zwischen diesen beiden untergeordneten Elementen zu erstellen, kann eine zusätzliche Unterebene verwendet werden. Zum Beispiel:

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");

Dadurch wird ein Knoten mit dem HierarchyId-Wert /4/1/3/1/1.5/ erstellt, wodurch er zwischen child1 und child2 liegt.

Diese Verbesserung wurde von @Rezakazemi890 beigesteuert. Danke vielmals!

Tools

Weniger Neuerstellungen

Das Befehlszeilentool dotnet ef erstellt standardmäßig Ihr Projekt, bevor das Tool ausgeführt wird. Der Grund dafür ist, dass die Ausführung des Tools ohne vorherige Neuerstellung eine häufige Quelle der Verwirrung ist, wenn Dinge nicht funktionieren. Erfahrene Entwickler können die Option --no-build verwenden, um diesen Build zu vermeiden, der unter Umständen langsam ist. Aber auch die Option --no-build kann dazu führen, dass das Projekt neu erstellt wird, wenn es das nächste Mal außerhalb der EF-Tools erstellt wird.

Wir glauben, dass ein Communitybeitrag von @Suchiman dies behoben hat. Wir sind uns jedoch auch bewusst, dass Änderungen am MSBuild-Verhalten zu unbeabsichtigten Konsequenzen führen können. Daher bitten wir Personen wie Sie, dies auszuprobieren und uns über etwaige negative Erfahrungen zu berichten.