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. Details finden Sie unter Plan für Entity Framework Core 9.
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
Wir arbeiten an wichtigen Updates in EF9 für den EF Core-Datenbankanbieter für Azure Cosmos DB für NoSQL.
Rollenbasierter Zugriff
Azure Cosmos DB für NoSQL umfasst ein integriertes rollenbasiertes Zugriffssteuerungssystem (RBAC). Dies wird jetzt von EF9 sowohl für die Verwaltung als auch für die Verwendung von Containern unterstützt. Für Anwendungscode sind keine Änderungen erforderlich. Weitere Informationen finden Sie unter Issue Nr. 32197.
Synchroner Zugriff standardmäßig blockiert
Tipp
Der hier gezeigte Code stammt aus CosmosSyncApisSample.cs.
Azure Cosmos DB für NoSQL unterstützt keinen synchronen (blockierenden) Zugriff vom Anwendungscode. Zuvor hat EF dies standardmäßig durch Blockieren für asynchrone Aufrufe maskiert. Dies fördert jedoch die Synchronisierungsverwendung, was eine schlechte Praxis ist, und zu Deadlocks führen kann. Daher wird ab EF9 eine Ausnahme ausgelöst, wenn der synchrone Zugriff versucht wird. Zum Beispiel:
System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.SyncNotSupported':
Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using
Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
This exception can be suppressed or logged by passing event ID 'CosmosEventId.SyncNotSupported' to the 'ConfigureWarnings'
method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
at Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal.CosmosLoggerExtensions.SyncNotSupported(IDiagnosticsLogger`1 diagnostics)
at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosClientWrapper.DeleteDatabase()
at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosDatabaseCreator.EnsureDeleted()
at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()
Wie die Ausnahme besagt, kann der Synchronisierungszugriff jetzt weiterhin 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 die Synchronisierungsunterstützung in EF11 vollständig entfernen möchten. Beginnen Sie daher mit der Aktualisierung, asynchrone Methoden wie ToListAsync
und SaveChangesAsync
so schnell wie möglich zu verwenden!
Erweiterte einfache Sammlungen
Tipp
Der hier gezeigte Code stammt aus CosmosPrimitiveTypesSample.cs.
Der Cosmos DB-Anbieter unterstützt einfache Sammlungen in begrenzter Form seit EF Core 6. Diese Unterstützung wird in EF9 verbessert, beginnend mit der Konsolidierung der Metadaten und API-Oberflächen für primitive Sammlungen in Dokumentdatenbanken, um mit einfachen Sammlungen in relationalen Datenbanken auszurichten. Dies bedeutet, dass einfache Sammlungen jetzt explizit mithilfe der Modellbau-API zugeordnet werden können, sodass Facets des Elementtyps konfiguriert werden können. So ordnen Sie beispielsweise eine Liste der erforderlichen Zeichenfolgen (d. h. nicht null) zu:
modelBuilder.Entity<Book>()
.PrimitiveCollection(e => e.Quotes)
.ElementType(b => b.IsRequired());
Weitere Informationen zur Modellerstellungs-API finden Sie unter Neuerungen in EF8: einfache Sammlungen.
AOT und vorab kompilierte Abfragen
Wie in der Einführung erwähnt, gibt es hinter den Kulissen viel Arbeit, um EF Core ohne Just-in-Time-Kompilierung (JIT) auszuführen. Stattdessen kompilieren EF vorab (AOT) alles, was zum Ausführen von Abfragen in der Anwendung erforderlich ist. Diese AOT-Kompilierung und zugehörige Verarbeitung erfolgt im Rahmen der Erstellung und Veröffentlichung der Anwendung. An diesem Punkt in der EF9-Version ist nicht viel verfügbar, das von Ihnen, dem App-Entwickler, verwendet werden kann. Für interessierte Personen sind jedoch die abgeschlossenen Probleme in EF9, die AOT und vorab kompilierte Abfragen unterstützen:
- Kompiliertes Modell: Verwenden Sie statische Bindung anstelle der Spiegelung für Eigenschaften und Felder
- Kompiliertes Modell: Generieren von Lambdas, die in der Änderungsnachverfolgung
- Änderungsnachverfolgung und die Updatepipeline mit AOT/Trimming
- Verwenden von Interceptors zum Umleiten der Abfrage zu vorkompiliertem Code
- Alle SQL-Ausdrucksknoten
- Generieren des kompilierten Modells während des Build
- Automatisches Ermitteln des kompilierten Modells
- Ermöglichen Sie es dem ParameterExtractingExpressionVisitor, Pfade zu auswertbaren Fragmenten im Baum zu extrahieren
- Generieren von Ausdrucksstrukturen in kompilierten Modellen (Abfragefilter, Wertkonverter)
- Erstellen von LinqToCSharpSyntaxTranslator stabiler gegenüber mehreren Deklarationen derselben Variable in geschachtelten Bereichen
- Optimieren Sie ParameterExtractingExpressionVisitor
Sehen Sie sich hier Beispiele für die Verwendung von vorab kompilierten Abfragen an, wenn die Oberfläche zusammenkommt.
LINQ- und SQL-Übersetzung
Das Team arbeitet an einigen bedeutenden Architekturänderungen an der Abfragepipeline in EF Core 9 als Teil unserer kontinuierlichen Verbesserungen des JSON-Mappings und der Dokumentendatenbanken. Dies bedeutet, dass wir Personen wie Sie brauchen, um Ihren Code mit diesen neuen internen Versionen auszuführen. (Wenn Sie an dieser Stelle des Releases ein Dokument zu Neuerungen lesen, dann sind Sie ein wirklich engagierter Teil der Community; vielen Dank!) Wir haben über 120.000 Tests durchgeführt, aber das reicht noch nicht. Wir benötigen Sie, Menschen die echten Code in unseren Produkten ausführen, um Probleme zu finden und ein stabiles Release zu liefern.
Komplexe GroupBy-Typen
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]
Löschen von Spalten, die an die WITH-Klausel von OPENJSON übergeben werden
Tipp
Der hier gezeigte Code stammt aus JsonColumnsSample.cs.
EF9 entfernt beim Aufrufen von OPENJSON WITH
unnötige Spalten. Nehmen Sie eine Abfrage als Beispiel, die mithilfe eines Prädikats eine Anzahl aus einer JSON-Collection abruft:
var postsUpdatedOn = await context.Posts
.Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
.ToListAsync();
In EF8 generiert diese Abfrage bei Verwendung des Azure SQL-Datenbankanbieters den folgenden SQL-Code:
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 (
SELECT COUNT(*)
FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
[PostedFrom] nvarchar(45) '$.PostedFrom',
[UpdatedBy] nvarchar(max) '$.UpdatedBy',
[UpdatedOn] date '$.UpdatedOn',
[Commits] nvarchar(max) '$.Commits' AS JSON
) AS [u]
WHERE [u].[UpdatedOn] >= @__date_0) = 1
Sie sehen, dass UpdatedBy
und Commits
in dieser Abfrage nicht benötigt werden. Ab EF9 werden diese Spalten jetzt gelöscht:
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 (
SELECT COUNT(*)
FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
[PostedFrom] nvarchar(45) '$.PostedFrom',
[UpdatedOn] date '$.UpdatedOn'
) AS [u]
WHERE [u].[UpdatedOn] >= @__date_0) = 1
In einigen Szenarios führt das dazu, dass die WITH
-Klausel vollständig entfernt wird. Beispiel:
var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();
In EF8 wird diese Abfrage in den folgenden SQL-Code übersetzt:
SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
SELECT COUNT(*)
FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1
In EF9 wurde dieser verbessert zu:
SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
SELECT COUNT(*)
FROM OPENJSON([t].[Text]) AS [t0]) = 1
Ü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:
info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
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 führt in diesem Fall zu vielen Abfragen im Abfragecache, bei denen sich nur die Parameterwerte 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:
info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
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
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“:
info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
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
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 IQueryable
inline in dotnetPosts
, was zu einem einzelnen 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
Neue ToHashSetAsync<T>
-Methoden
Tipp
Der hier gezeigte Code stammt aus QuerySample.cs.
Die Enumerable.ToHashSet-Methoden gibt es seit .NET Core 2.0. In EF9 wurden die entsprechenden asynchronen Methoden hinzugefügt. Zum Beispiel:
var set1 = await context.Posts
.Where(p => p.Tags.Count > 3)
.ToHashSetAsync();
var set2 = await context.Posts
.Where(p => p.Tags.Count > 3)
.ToHashSetAsync(ReferenceEqualityComparer.Instance);
Diese Verbesserung wurde von @wertzui beigesteuert. Danke vielmals!
ExecuteUpdate und ExecuteDelete
Zulassen des Übergebens komplexer Typinstanzen an ExecuteUpdate
Tipp
Der hier gezeigte Code stammt aus ExecuteUpdateSample.cs.
Die ExecuteUpdate
-API wurde in EF7 eingeführt, um sofortige, direkte Aktualisierungen der Datenbank ohne Nachverfolgung oder SaveChanges
durchzuführen. Beispiel:
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Region, "Deutschland"));
Wenn Sie diesen Code ausführen, wird die folgende Abfrage ausgelöst, um Region
in „Deutschland“ zu ändern:
UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
In EF8 kann ExecuteUpdate
auch verwendet werden, um Werte komplexer Typeigenschaften zu aktualisieren. Jedes Element des komplexen Typs muss jedoch explizit angegeben werden. Beispiel:
var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");
await context.Stores
.Where(e => e.Region == "Deutschland")
.ExecuteUpdateAsync(
s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
.SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
.SetProperty(b => b.StoreAddress.City, newAddress.City)
.SetProperty(b => b.StoreAddress.Country, newAddress.Country)
.SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));
Das Ausführen dieses Codes löst die folgende Abfrage aus:
UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
[s].[StoreAddress_Country] = @__newAddress_Country_3,
[s].[StoreAddress_City] = @__newAddress_City_2,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'
In EF9 kann dasselbe Update ausgeführt werden, indem die komplexe Typinstanz selbst übergeben wird. Das heißt, dass nicht jeder Member explizit angegeben werden muss. 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));
Das Ausführen dieses Codes löst die gleiche Abfrage wie im vorherigen Beispiel aus:
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'
Mehrere Aktualisierungen von komplexen Typeigenschaften und einfachen Eigenschaften können in einem einzigen Aufruf von ExecuteUpdate
kombiniert werden. Beispiel:
await context.Customers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(
s => s.SetProperty(
b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
.SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
.SetProperty(b => b.CustomerInfo.Tag, "Tog"));
Das Ausführen dieses Codes löst die gleiche Abfrage wie im vorherigen Beispiel aus:
UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
[c].[CustomerInfo_HomeAddress_City] = N'Beetley',
[c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
[c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
[c].[CustomerInfo_HomeAddress_Line2] = NULL,
[c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
[c].[CustomerInfo_WorkAddress_City] = N'Beetley',
[c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
[c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
[c].[CustomerInfo_WorkAddress_Line2] = NULL,
[c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0
Migrationen
Verbesserte Migration von temporären Tabellen
Die Migration beim Ändern einer vorhandenen Tabelle in eine temporäre Tabelle wurde für EF9 verkleinert. Wenn in EF8 beispielsweise eine einzelne vorhandene Tabelle in eine temporäre Tabelle umgewandelt wird, führt das zu folgender Migration:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "SiteUri",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Blogs",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("SqlServer:Identity", "1, 1")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
.OldAnnotation("SqlServer:Identity", "1, 1");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}
In EF9 führt derselbe Vorgang nun zu einer viel kleineren Migration:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodEndColumn", true);
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}
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 MSBuild ausgeliefert und zielt auf das Paket ab, 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-preview.4.24205.3
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
-Eigenschaft auf Ihre .csproj
-Datei festlegen. Zum Beispiel:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>
Es gibt zusätzliche, optionale MSBuild-Eigenschaften zum Steuern der Erstellung des Modells, die den Optionen entsprechen, die an die Befehlszeile an dotnet ef dbcontext optimize
übergeben werden. Dazu gehören:
MSBuild-Eigenschaft | Beschreibung |
---|---|
EFOptimizeContext | Legen Sie auf true fest, um automatisch kompilierte Modelle zu aktivieren. |
DbContextName | Die zu verwendende DbContext-Klasse. Nur Klassenname oder vollqualifiziert mit Namespaces. Wenn diese Option ausgelassen wird, findet EF Core die Kontextklasse. Wenn mehrere Kontextklassen vorhanden sind, ist diese Option erforderlich. |
EFStartupProject | Relativer Pfad zum Startprojekt. Der Standardwert ist der aktuelle Ordner. |
EFTargetNamespace | Der für alle generierten Klassen zu verwendende Namespace. Standardmäßig werden aus dem Stammnamespace und dem Ausgabeverzeichnis plus CompiledModels generiert. |
In unserem Beispiel müssen wir das Startprojekt angeben:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFStartupProject>..\App\App.csproj</EFStartupProject>
</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 --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.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.
[HINWEIS!] Wir arbeiten an einigen Leistungsproblemen mit Änderungen am kompilierten Modell in EF8 und EF9. Weitere Informationen finden Sie unter Issue Nr. 33483.
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"
Zwischenspeichern für Sequenzen festlegen
Tipp
Der hier gezeigte Code stammt aus ModelBuildingSample.cs.
EF9 ermöglicht das Festlegen der Zwischenspeicherungsoptionen für Datenbanksequenzen für jeden Anbieter von relationalen Datenbanken, der dies unterstützt. Beispielsweise kann UseCache
verwendet werden, um die Zwischenspeicherung explizit zu aktivieren und die Cachegröße festzulegen:
modelBuilder.HasSequence<int>("MyCachedSequence")
.HasMin(10).HasMax(255000)
.IsCyclic()
.StartsAt(11).IncrementsBy(2)
.UseCache(3);
Dies führt bei Verwendung von SQL Server zu folgender Sequenzdefinition:
CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;
Ebenso deaktiviert UseNoCache
die Zwischenspeicherung explizit:
modelBuilder.HasSequence<int>("MyUncachedSequence")
.HasMin(10).HasMax(255000)
.IsCyclic()
.StartsAt(11).IncrementsBy(2)
.UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;
Wenn weder UseCache
noch UseNoCache
aufgerufen werden, wird keine Zwischenspeicherung festgelegt, und die Datenbank verwendet den Standardwert. Dies kann für verschiedene Datenbanken ein anderer Standard sein.
Diese Verbesserung wurde von @bikbov beigesteuert. Danke vielmals!
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.
Feedback
https://aka.ms/ContentUserFeedback.
Bald verfügbar: Im Laufe des Jahres 2024 werden wir GitHub-Issues stufenweise als Feedbackmechanismus für Inhalte abbauen und durch ein neues Feedbacksystem ersetzen. Weitere Informationen finden Sie unterFeedback senden und anzeigen für