Share via


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:

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 ReadOnlyCollectioneingegeben werden. Im folgenden Code wird DaysVisited z. B. gemäß der Konvention als einfache Sammlung von Datumsangaben zugeordnet:

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

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

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

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

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

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

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

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

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.