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
,Contains
und viele andere. - Unterstützung von Aggregatoperatoren wie
Count
undSum
. - Zusätzliche Funktionsübersetzungen (siehe Dokumentation zu Funktionszuordnungen für die vollständige Liste der unterstützten Übersetzungen):
- Übersetzungen für Komponentenmember
DateTime
undDateTimeOffset
(DateTime.Year
,DateTimeOffset.Month
...). EF.Functions.IsDefined
undEF.Functions.CoalesceUndefined
lassen jetzt den Umgang mitundefined
Werten zu.string.Contains
,StartsWith
undEndsWith
unterstützt jetztStringComparison.OrdinalIgnoreCase
.
- Übersetzungen für Komponentenmember
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 TranslateParameterizedCollectionsToConstants
Kontextoption 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 erweitert
ICollection<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 eineHashSet
(#30033, von @wertzui beigetragen) zurückzugeben. TimeOnly.FromDateTime
undFromTimeSpan
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-FunktionPATINDEX
übersetzt, die die Startposition des ersten Vorkommens eines Musters (#33702, @smnsht) zurückgibt.Sum
undAverage
arbeiten jetzt für Dezimalstellen auf SQLite (#33721, von @ranma42 beigetragen).- Korrekturen und Optimierungen an
string.StartsWith
undEndsWith
(#31482). Convert.To*
Methoden können jetzt Argument vom Typobject
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
undAT TIME ZONE
(#34263, von @ranma42 beigetragen). - Optimierungen für
DISTINCT
überIN
-,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 ReadOnlyCollection
eingegeben 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.