Co nowego w programie EF Core 9

EF Core 9 (EF9) to kolejna wersja po programie EF Core 8 i zaplanowana na wydanie w listopadzie 2024 r. Aby uzyskać szczegółowe informacje, zobacz Plan dla programu Entity Framework Core 9 .

Program EF9 jest dostępny jako codzienne kompilacje , które zawierają wszystkie najnowsze funkcje ef9 i poprawki interfejsu API. Przykłady w tym miejscu korzystają z tych codziennych kompilacji.

Napiwek

Przykładowe przykłady można uruchamiać i debugować , pobierając przykładowy kod z usługi GitHub. Każda poniższa sekcja zawiera linki do kodu źródłowego specyficznego dla tej sekcji.

Program EF9 jest przeznaczony dla platformy .NET 8 i dlatego może być używany z platformą .NET 8 (LTS) lub platformą .NET 9 (wersja zapoznawcza).

Napiwek

Dokumentacja co nowego jest aktualizowana dla każdej wersji zapoznawczej. Wszystkie przykłady są skonfigurowane do korzystania z codziennych kompilacji EF9, które zwykle mają kilka dodatkowych tygodni ukończonych prac w porównaniu do najnowszej wersji zapoznawczej. Zdecydowanie zachęcamy do korzystania z codziennych kompilacji podczas testowania nowych funkcji, aby nie wykonywać testów względem nieaktualnych bitów.

Azure Cosmos DB for NoSQL

Pracujemy nad znaczącymi aktualizacjami w programie EF9 u dostawcy bazy danych EF Core dla usługi Azure Cosmos DB for NoSQL.

Dostęp oparty na rolach

Usługa Azure Cosmos DB for NoSQL zawiera wbudowany system kontroli dostępu opartej na rolach (RBAC). Jest to teraz obsługiwane przez program EF9 zarówno do zarządzania kontenerami, jak i korzystania z nich. Do kodu aplikacji nie są wymagane żadne zmiany. Aby uzyskać więcej informacji, zobacz Problem nr 32197 .

Dostęp synchroniczny domyślnie zablokowany

Napiwek

Pokazany tutaj kod pochodzi z CosmosSyncApisSample.cs.

Usługa Azure Cosmos DB for NoSQL nie obsługuje dostępu synchronicznego (blokowania) z kodu aplikacji. Wcześniej program EF maskował to domyślnie przez blokowanie w przypadku wywołań asynchronicznych. Jednak obie te metody zachęcają do używania synchronizacji, co jest złym rozwiązaniem i może spowodować zakleszczenia. W związku z tym, począwszy od ef9, podczas próby synchronicznego dostępu jest zgłaszany wyjątek. Na przykład:

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()

Jak mówi wyjątek, dostęp do synchronizacji może być nadal używany na razie przez odpowiednie skonfigurowanie poziomu ostrzeżenia. Na przykład w OnConfiguring typie DbContext :

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

Należy jednak pamiętać, że planujemy w pełni usunąć obsługę synchronizacji w programie EF11, więc zacznij aktualizować się tak szybko, aby używać metod asynchronicznych, takich jak ToListAsync i SaveChangesAsync jak najszybciej!

Rozszerzone kolekcje pierwotne

Napiwek

Pokazany tutaj kod pochodzi z CosmosPrimitiveTypesSample.cs.

Dostawca usługi Cosmos DB obsługuje kolekcje pierwotne w ograniczonej formie od czasu platformy EF Core 6. Ta obsługa jest rozszerzana w programie EF9, począwszy od konsolidacji metadanych i powierzchni interfejsu API dla kolekcji pierwotnych w bazach danych dokumentów w celu dopasowania ich do kolekcji pierwotnych w relacyjnych bazach danych. Oznacza to, że kolekcje pierwotne można teraz jawnie mapować przy użyciu interfejsu API tworzenia modelu, co umożliwia skonfigurowanie aspektów typu elementu. Aby na przykład zamapować listę wymaganych ciągów (tj. ciągów innych niż null):

modelBuilder.Entity<Book>()
    .PrimitiveCollection(e => e.Quotes)
    .ElementType(b => b.IsRequired());

Aby uzyskać więcej informacji na temat interfejsu API tworzenia modelu, zobacz Co nowego w programie EF8: kolekcje pierwotne.

Zapytania AOT i wstępnie skompilowane

Jak wspomniano we wprowadzeniu, istnieje wiele prac w tle, aby umożliwić programowi EF Core uruchamianie bez kompilacji just in time (JIT). Zamiast tego program EF skompiluje wszystko, co jest potrzebne do uruchamiania zapytań w aplikacji. Ta kompilacja AOT i powiązane przetwarzanie będą wykonywane w ramach kompilowania i publikowania aplikacji. W tym momencie w wersji EF9 nie ma zbyt wiele dostępnych, które mogą być używane przez Ciebie, deweloper aplikacji. Jednak dla osób zainteresowanych ukończone problemy w programie EF9, które obsługują usługę AOT i wstępnie skompilowane zapytania, to:

Zapoznaj się z tym artykułem, aby zapoznać się z przykładami użycia wstępnie skompilowanych zapytań podczas łączenia środowiska.

TŁUMACZENIE LINQ i SQL

Zespół pracuje nad istotnymi zmianami architektury potoku zapytań w programie EF Core 9 w ramach naszych ciągłych ulepszeń mapowania JSON i baz danych dokumentów. Oznacza to, że musimy zapewnić innym osobom, jak Ty , aby uruchamiali kod w tych nowych elementach wewnętrznych. (Jeśli czytasz dokument "Co nowego" w tym momencie w wydaniu, jesteś naprawdę zaangażowaną częścią społeczności; dziękuję!) Mamy ponad 120.000 testów, ale to nie wystarczy! Potrzebujemy Ciebie, ludzi, którzy uruchamiają prawdziwy kod na naszych bitach, aby znaleźć problemy i wysłać solidną wersję!

Grupuj według typów złożonych

Napiwek

Pokazany tutaj kod pochodzi z ComplexTypesSample.cs.

Program EF9 obsługuje grupowanie według wystąpienia typu złożonego. Na przykład:

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

Ef tłumaczy to jako grupowanie według każdego elementu członkowskiego typu złożonego, który jest zgodny z semantyka typów złożonych jako obiekty wartości. Na przykład w usłudze 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]

Przycinanie kolumn przekazanych do klauzuli WITH pliku OPENJSON

Napiwek

Pokazany tutaj kod pochodzi z JsonColumnsSample.cs.

Program EF9 usuwa niepotrzebne kolumny podczas wywoływania elementu OPENJSON WITH. Rozważmy na przykład zapytanie, które uzyskuje liczbę z kolekcji JSON przy użyciu predykatu:

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

W programie EF8 to zapytanie generuje następujący kod SQL podczas korzystania z dostawcy usługi Azure SQL Database:

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

Zwróć uwagę, że element UpdatedByi Commits nie są potrzebne w tym zapytaniu. Począwszy od ef9, te kolumny są teraz przycinane:

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

W niektórych scenariuszach powoduje to całkowite usunięcie klauzuli WITH . Na przykład:

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

W programie EF8 to zapytanie przekłada się na następujący kod SQL:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

W programie EF9 zostało to ulepszone w następujący sposób:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

Tłumaczenia z udziałem GREATEST/LEAST

Napiwek

Pokazany tutaj kod pochodzi z LeastGreatestSample.cs.

Wprowadzono kilka nowych tłumaczeń korzystających z GREATEST funkcji i LEAST SQL.

Ważne

Funkcje GREATESTi LEAST wprowadzone w bazach danych SQL Server/Azure SQL Database w wersji 2022. Program Visual Studio 2022 domyślnie instaluje program SQL Server 2019. Zalecamy zainstalowanie programu SQL Server Developer Edition 2022 , aby wypróbować te nowe tłumaczenia w programie EF9.

Na przykład zapytania używające lub Math.MaxMath.Min są teraz tłumaczone dla usługi Azure SQL przy użyciu i GREATESTLEAST odpowiednio. Na przykład:

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

To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu EF9 wykonującego względem programu SQL Server 2022:

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 można Math.Max również używać wartości kolekcji pierwotnej. Na przykład:

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

To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu EF9 wykonującego względem programu SQL Server 2022:

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

RelationalDbFunctionsExtensions.Least Na koniec i RelationalDbFunctionsExtensions.Greatest może służyć do bezpośredniego wywoływania Least funkcji or Greatest w języku SQL. Na przykład:

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

To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu EF9 wykonującego względem programu SQL Server 2022:

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]

Wymuszanie lub zapobieganie parametryzacji zapytań

Napiwek

Pokazany tutaj kod pochodzi z QuerySample.cs.

Z wyjątkiem niektórych specjalnych przypadków, EF Core parametryzuje zmienne używane w zapytaniu LINQ, ale zawiera stałe w wygenerowanym języku SQL. Rozważmy na przykład następującą metodę zapytania:

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

Przekłada się to na następujące parametry SQL i podczas korzystania z usługi Azure SQL:

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

Zwróć uwagę, że program EF utworzył stałą w języku SQL dla bloga platformy ".NET", ponieważ ta wartość nie zmieni się z zapytania na kwerendę. Użycie stałej umożliwia badanie tej wartości przez aparat bazy danych podczas tworzenia planu zapytania, co może spowodować zwiększenie wydajności zapytania.

Z drugiej strony wartość parametru id jest sparametryzowana, ponieważ to samo zapytanie może być wykonywane z wieloma różnymi wartościami dla elementu id. Utworzenie stałej w tym przypadku powoduje zanieczyszczenie pamięci podręcznej zapytań z dużą częścią zapytań, które różnią się tylko wartościami parametrów. Jest to bardzo złe w przypadku ogólnej wydajności bazy danych.

Mówiąc ogólnie, te wartości domyślne nie powinny być zmieniane. Jednak program EF Core 8.0.2 wprowadza metodę EF.Constant , która wymusza użycie stałej przez program EF, nawet jeśli parametr będzie używany domyślnie. Na przykład:

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

Tłumaczenie zawiera teraz stałą dla id wartości:

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

Program EF9 wprowadza metodę EF.Parameter do wykonania odwrotnego. Oznacza to, że wymuś użycie parametru przez program EF, nawet jeśli wartość jest stałą w kodzie. Na przykład:

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

Tłumaczenie zawiera teraz parametr ciągu bloga ".NET":

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

Niezrelowane podzapytania

Napiwek

Pokazany tutaj kod pochodzi z QuerySample.cs.

W programie EF8 zapytanie IQueryable, do których odwołuje się inne zapytanie, może być wykonywane jako oddzielna dwukierunkowa baza danych. Rozważmy na przykład następujące zapytanie LINQ:

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();

W programie EF8 zapytanie jest dotnetPosts wykonywane jako jedna runda, a następnie końcowe wyniki są wykonywane jako drugie zapytanie. Na przykład w programie 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

W programie EF9 IQueryable element w elemecie dotnetPosts jest podkreślony, co powoduje pojedynczą rundę:

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

Nowe ToHashSetAsync<T> metody

Napiwek

Pokazany tutaj kod pochodzi z QuerySample.cs.

Metody Enumerable.ToHashSet istniały od platformy .NET Core 2.0. W programie EF9 dodano równoważne metody asynchroniczne. Na przykład:

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);

To ulepszenie zostało wprowadzone przez @wertzui. Dziękujemy!

ExecuteUpdate i ExecuteDelete

Zezwalaj na przekazywanie wystąpień typu złożonego do polecenia ExecuteUpdate

Napiwek

Pokazany tutaj kod pochodzi z ExecuteUpdateSample.cs.

Interfejs ExecuteUpdate API został wprowadzony w programie EF7 w celu natychmiastowego, bezpośredniego aktualizowania bazy danych bez śledzenia lub SaveChanges. Na przykład:

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

Uruchomienie tego kodu powoduje wykonanie następującego zapytania w celu zaktualizowania elementu Region do elementu "Deutschland":

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

W programie EF8 ExecuteUpdate można również użyć do aktualizowania wartości właściwości typu złożonego. Jednak każdy element członkowski typu złożonego musi być określony jawnie. Na przykład:

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));

Uruchomienie tego kodu powoduje wykonanie następującego zapytania:

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'

W programie EF9 tę samą aktualizację można wykonać przez przekazanie samego wystąpienia typu złożonego. Oznacza to, że każdy element członkowski nie musi być jawnie określony. Na przykład:

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));

Uruchomienie tego kodu powoduje wykonanie tego samego zapytania co w poprzednim przykładzie:

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'

Wiele aktualizacji zarówno właściwości typu złożonego, jak i prostych właściwości można połączyć w jednym wywołaniu metody ExecuteUpdate. Na przykład:

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"));

Uruchomienie tego kodu powoduje wykonanie tego samego zapytania co w poprzednim przykładzie:

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

Migracje

Ulepszone migracje tabel czasowych

Migracja utworzona podczas zmiany istniejącej tabeli na tabelę czasową została zmniejszona dla programu EF9. Na przykład w programie EF8 utworzenie pojedynczej istniejącej tabeli powoduje wykonanie następującej migracji:

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");
}

W programie EF9 ta sama operacja powoduje teraz znacznie mniejszą migrację:

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);
}

Kompilowanie modelu

Modele kompilowane automatycznie

Napiwek

Pokazany tutaj kod pochodzi z przykładu NewInEFCore9.CompiledModels .

Skompilowane modele mogą poprawić czas uruchamiania aplikacji z dużymi modelami — jest to liczba typów jednostek w 100 lub 1000. W poprzednich wersjach programu EF Core skompilowany model musiał zostać wygenerowany ręcznie przy użyciu wiersza polecenia. Na przykład:

dotnet ef dbcontext optimize

Po uruchomieniu polecenia należy dodać wiersz podobny do polecenia , .UseModel(MyCompiledModels.BlogsContextModel.Instance) aby OnConfiguring poinformować platformę EF Core o użyciu skompilowanego modelu.

Począwszy od ef9, ten .UseModel wiersz nie jest już potrzebny, gdy typ aplikacji DbContext znajduje się w tym samym projekcie/zestawie co skompilowany model. Zamiast tego skompilowany model zostanie wykryty i użyty automatycznie. Można to zobaczyć, logując program EF za każdym razem, gdy kompiluje model. Uruchomienie prostej aplikacji powoduje wyświetlenie kompilowania modelu przez platformę EF po uruchomieniu aplikacji:

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

Dane wyjściowe z uruchamiania dotnet ef dbcontext optimize w projekcie modelu to:

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> 

Zwróć uwagę, że dane wyjściowe dziennika wskazują, że model został skompilowany podczas uruchamiania polecenia. Jeśli teraz ponownie uruchomimy aplikację, po ponownym skompilowaniu, ale bez wprowadzania żadnych zmian w kodzie, dane wyjściowe to:

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

Zwróć uwagę, że model nie został skompilowany podczas uruchamiania aplikacji, ponieważ skompilowany model został wykryty i użyty automatycznie.

Integracja z programem MSBuild

W przypadku powyższego podejścia skompilowany model nadal musi być ponownie wygenerowany ręcznie po zmianie typów jednostek lub DbContext konfiguracji. Jednak program EF9 jest dostarczany z programem MSBuild i pakietem docelowym, który może automatycznie aktualizować skompilowany model po skompilowaniu projektu modelu. Aby rozpocząć, zainstaluj pakiet NuGet Microsoft.EntityFrameworkCore.Tasks . Na przykład:

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

Napiwek

Użyj wersji pakietu w poleceniu powyżej, który pasuje do używanej wersji programu EF Core.

Następnie włącz integrację, ustawiając EFOptimizeContext właściwość na plik .csproj . Na przykład:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

Istnieją dodatkowe, opcjonalne właściwości programu MSBuild do kontrolowania sposobu kompilowania modelu, co odpowiada opcjom przekazywanym w wierszu polecenia do dotnet ef dbcontext optimize. Są to:

Właściwość MSBuild opis
EFOptimizeContext Ustaw wartość na , aby true włączyć modele kompilowane automatycznie.
DbContextName Klasa DbContext do użycia. Tylko nazwa klasy lub w pełni kwalifikowana z przestrzeniami nazw. Jeśli ta opcja zostanie pominięta, program EF Core znajdzie klasę kontekstu. Jeśli istnieje wiele klas kontekstowych, ta opcja jest wymagana.
EFStartupProject Względna ścieżka do projektu startowego. Wartość domyślna to bieżący folder.
EFTargetNamespace Przestrzeń nazw do użycia dla wszystkich wygenerowanych klas. Domyślnie są generowane z głównej przestrzeni nazw i katalogu wyjściowego oraz CompiledModels.

W naszym przykładzie musimy określić projekt startowy:

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
  <EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>

Teraz, jeśli skompilujemy projekt, zobaczymy rejestrowanie w czasie kompilacji wskazujące, że kompilowany model jest kompilowany:

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 

Uruchomiona aplikacja pokazuje, że skompilowany model został wykryty, dlatego model nie został skompilowany ponownie:

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

Teraz, gdy tylko zmieni się model, skompilowany model zostanie automatycznie skompilowany ponownie utworzony.

[UWAGA!] Pracujemy nad niektórymi problemami z wydajnością dotyczącymi zmian wprowadzonych w skompilowanym modelu w programach EF8 i EF9. Aby uzyskać więcej informacji, zobacz Problem 33483# .

Kolekcje pierwotne tylko do odczytu

Napiwek

Pokazany tutaj kod pochodzi z PrimitiveCollectionsSample.cs.

Program EF8 wprowadził obsługę mapowania tablic i modyfikowalnych list typów pierwotnych. Ta funkcja została rozszerzona w programie EF9 w celu uwzględnienia kolekcji/list tylko do odczytu. W szczególności program EF9 obsługuje kolekcje wpisane jako IReadOnlyList, IReadOnlyCollectionlub ReadOnlyCollection. Na przykład w poniższym kodzie DaysVisited zostanie zamapowany zgodnie z konwencją jako pierwotna kolekcja dat:

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

Kolekcja tylko do odczytu może być wspierana przez normalną, modyfikowaną kolekcję w razie potrzeby. Na przykład w poniższym kodzie DaysVisited można mapować jako pierwotną kolekcję dat, pozwalając jednocześnie kodowi w klasie manipulować bazową listą.

    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;
    }

Te kolekcje mogą być następnie używane w zapytaniach w normalny sposób. Na przykład to zapytanie LINQ:

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();

Co przekłada się na następujący kod SQL w języku 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"

Określanie buforowania dla sekwencji

Napiwek

Pokazany tutaj kod pochodzi z ModelBuildingSample.cs.

Program EF9 umożliwia ustawienie opcji buforowania dla sekwencji baz danych dla dowolnego dostawcy relacyjnej bazy danych, który to obsługuje. Na przykład UseCache można użyć do jawnego włączenia buforowania i ustawienia rozmiaru pamięci podręcznej:

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

Powoduje to następującą definicję sekwencji podczas korzystania z programu SQL Server:

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

Podobnie jawnie UseNoCache wyłącza buforowanie:

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;

Jeśli ani nie UseCacheUseNoCache zostanie wywołana, buforowanie nie zostanie określone, a baza danych będzie używać dowolnej wartości domyślnej. Może to być inna wartość domyślna dla różnych baz danych.

To ulepszenie zostało wprowadzone przez @bikbov. Dziękujemy!

Określanie współczynnika wypełnienia dla kluczy i indeksów

Napiwek

Pokazany tutaj kod pochodzi z ModelBuildingSample.cs.

Program EF9 obsługuje specyfikację współczynnika wypełnienia programu SQL Server podczas używania migracji platformy EF Core do tworzenia kluczy i indeksów. W dokumentacji programu SQL Server "Podczas tworzenia lub odbudowy indeksu wartość współczynnika wypełnienia określa procent miejsca na każdej stronie na poziomie liścia, rezerwując resztę na każdej stronie jako wolne miejsce na przyszły wzrost".

Współczynnik wypełnienia można ustawić na jednym lub złożonym kluczu podstawowym i alternatywnym oraz indeksach. Na przykład:

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);

Po zastosowaniu do istniejących tabel spowoduje to zmianę tabel na współczynnik wypełnienia na ograniczenie:

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);

To ulepszenie zostało wprowadzone przez @deano-hunter. Dziękujemy!

Zwiększenie rozszerzalności istniejących konwencji tworzenia modelu

Napiwek

Pokazany tutaj kod pochodzi z CustomConventionsSample.cs.

Konwencje tworzenia modeli publicznych dla aplikacji zostały wprowadzone w programie EF7. W programie EF9 ułatwiliśmy rozszerzenie niektórych istniejących konwencji. Na przykład kod mapowania właściwości według atrybutu w programie EF7 to:

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;
            }
        }
    }
}

W programie EF9 można to uprościć do następujących elementów:

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;
    }
}

Zaktualizuj metodę ApplyConfigurationsFromAssembly, aby wywołać konstruktory niepublicznie

W poprzednich wersjach programu EF Core ApplyConfigurationsFromAssembly metoda tworzyła tylko wystąpienia typów konfiguracji z publicznymi konstruktorami bez parametrów. W programie EF9 ulepszyliśmy komunikaty o błędach generowane w przypadku niepowodzenia, a także włączono tworzenie wystąpień przez konstruktora niepublizowanego. Jest to przydatne w przypadku współlokowania konfiguracji w prywatnej zagnieżdżonej klasie, która nigdy nie powinna być tworzone przez kod aplikacji. Na przykład:

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);
        }
    }
}

Na bok niektórzy uważają, że ten wzorzec jest obrzydliwieniem, ponieważ łączy typ jednostki z konfiguracją. Inne osoby uważają, że jest to bardzo przydatne, ponieważ współlokuje konfigurację z typem jednostki. Nie dyskutujmy tego tutaj. :-)

Identyfikator hierarchii programu SQL Server

Napiwek

Pokazany tutaj kod pochodzi z HierarchyIdSample.cs.

Cukier dla generowania ścieżki HierarchyId

Obsługa pierwszej klasy dla typu programu SQL Server HierarchyId została dodana w programie EF8. W programie EF9 dodano metodę cukru, aby ułatwić tworzenie nowych węzłów podrzędnych w strukturze drzewa. Na przykład następujące zapytania dotyczące kodu dla istniejącej jednostki z właściwością HierarchyId :

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

Tej HierarchyId właściwości można następnie użyć do tworzenia węzłów podrzędnych bez jawnego manipulowania ciągami. Na przykład:

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

Jeśli daisy element ma HierarchyId wartość /4/1/3/1/z wartością , child1 otrzyma HierarchyId wartość "/4/1/3/1/1/" i child2 uzyska HierarchyId wartość "/4/1/3/1/2/".

Aby utworzyć węzeł między tymi dwoma elementami podrzędnym, można użyć dodatkowego poziomu podrzędnego. Na przykład:

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

Spowoduje to utworzenie węzła z elementem HierarchyId/4/1/3/1/1.5/, umieszczając go w pliku bteween child1 i child2.

To ulepszenie zostało wprowadzone przez @Rezakazemi890. Dziękujemy!

Narzędzia

Mniej ponownych kompilacji

Narzędzie dotnet ef wiersza polecenia domyślnie kompiluje projekt przed wykonaniem narzędzia. Jest to spowodowane tym, że nie jest to ponowne kompilowanie przed uruchomieniem narzędzia jest typowym źródłem pomyłek, gdy rzeczy nie działają. Doświadczeni deweloperzy mogą użyć --no-build opcji, aby uniknąć tej kompilacji, co może być powolne. Jednak nawet --no-build opcja może spowodować ponowne skompilowanie projektu przy następnym skompilowaniu poza narzędziami EF.

Uważamy, że wkład społeczności z @Suchiman rozwiązał ten problem. Jednak jesteśmy również świadomi, że poprawki dotyczące zachowań MSBuild mają tendencję do niezamierzonych konsekwencji, więc prosimy ludzi, którzy lubią cię wypróbować i zgłosić z powrotem na wszelkie negatywne doświadczenia, które masz.