Udostępnij za pośrednictwem


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.

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

Program EF 9.0 wprowadza znaczne ulepszenia dostawcy platformy EF Core dla usługi Azure Cosmos DB; znaczące części dostawcy zostały przepisane, aby zapewnić nowe funkcje, umożliwić nowe formy zapytań i lepiej dopasować dostawcę do najlepszych rozwiązań usługi Azure Cosmos DB. Poniżej wymieniono główne ulepszenia wysokiego poziomu; Aby zapoznać się z pełną listą, zobacz ten epicki problem.

Ostrzeżenie

W ramach ulepszeń przechodzących do dostawcy konieczne było wprowadzenie wielu zmian powodujących niezgodność; Jeśli uaktualniasz istniejącą aplikację, przeczytaj uważnie sekcję zmian powodujących niezgodność.

Ulepszenia wykonywania zapytań za pomocą kluczy partycji i identyfikatorów dokumentów

Każdy dokument przechowywany w bazie danych usługi Azure Cosmos DB ma unikatowy identyfikator zasobu. Ponadto każdy dokument może zawierać "klucz partycji", który określa partycjonowanie logiczne danych, tak aby można było efektywnie skalować bazę danych. Więcej informacji na temat wybierania kluczy partycji można znaleźć w temacie Partycjonowanie i skalowanie w poziomie w usłudze Azure Cosmos DB.

W programie EF 9.0 dostawca usługi Azure Cosmos DB jest znacznie lepszy w identyfikowaniu porównań kluczy partycji w zapytaniach LINQ i wyodrębnieniu ich w celu zapewnienia, że zapytania są wysyłane tylko do odpowiedniej partycji; może to znacznie poprawić wydajność zapytań i zmniejszyć opłaty za jednostki RU. Na przykład:

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

W tym zapytaniu dostawca automatycznie rozpoznaje porównanie . PartitionKeyJeśli sprawdzimy dzienniki, zobaczymy następujące elementy:

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

Należy pamiętać, że klauzula WHERE nie zawiera PartitionKeyelementu : to porównanie zostało "zniesione" i jest używane do wykonywania zapytania tylko względem odpowiedniej partycji. W poprzednich wersjach porównanie pozostało w klauzuli w WHERE wielu sytuacjach, powodując wykonanie zapytania względem wszystkich partycji i powodując zwiększenie kosztów i zmniejszenie wydajności.

Ponadto jeśli zapytanie udostępnia również wartość właściwości identyfikatora dokumentu i nie zawiera żadnych innych operacji zapytań, dostawca może zastosować dodatkową optymalizację:

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

Dzienniki zawierają następujące informacje dotyczące tego zapytania:

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

W tym miejscu żadne zapytanie SQL nie jest wysyłane w ogóle. Zamiast tego dostawca wykonuje niezwykle wydajny odczyt punktów (ReadItem INTERFEJS API), który bezpośrednio pobiera dokument przy użyciu klucza partycji i identyfikatora. Jest to najbardziej wydajny i ekonomiczny rodzaj odczytu, który można wykonać w usłudze Azure Cosmos DB; Aby uzyskać więcej informacji na temat odczytów punktów, zobacz dokumentację usługi Azure Cosmos DB.

Aby dowiedzieć się więcej na temat wykonywania zapytań za pomocą kluczy partycji i odczytów punktów, zobacz stronę dokumentacji dotyczącej wykonywania zapytań.

Hierarchiczne klucze partycji

Napiwek

Pokazany tutaj kod pochodzi z HierarchicalPartitionKeysSample.cs.

Usługa Azure Cosmos DB pierwotnie obsługiwała pojedynczy klucz partycji, ale od tego czasu rozszerza możliwości partycjonowania w celu obsługi partycjonowania za pomocą specyfikacji maksymalnie trzech poziomów hierarchii w kluczu partycji. Program EF Core 9 zapewnia pełną obsługę hierarchicznych kluczy partycji, umożliwiając korzystanie z lepszej wydajności i oszczędności kosztów związanych z tą funkcją.

Klucze partycji są określane przy użyciu interfejsu API tworzenia modelu, zazwyczaj w systemie DbContext.OnModelCreating. W typie jednostki musi istnieć zamapowana właściwość dla każdego poziomu klucza partycji. Rozważmy na przykład UserSession typ jednostki:

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

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

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

Poniższy kod określa klucz partycji na poziomie trzech poziomów TenantIdprzy użyciu właściwości , UserIdi SessionId :

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

Napiwek

Ta definicja klucza partycji jest zgodna z przykładem podanym w artykule Wybieranie kluczy partycji hierarchicznych z dokumentacji usługi Azure Cosmos DB.

Zwróć uwagę, że na początku programu EF Core 9 właściwości dowolnego typu mapowanego mogą być używane w kluczu partycji. W przypadku bool typów i liczbowych, takich jak int SessionId właściwość, wartość jest używana bezpośrednio w kluczu partycji. Inne typy, takie jak Guid UserId właściwość, są automatycznie konwertowane na ciągi.

Podczas wykonywania zapytań program EF automatycznie wyodrębnia wartości klucza partycji z zapytań i stosuje je do interfejsu API zapytań usługi Azure Cosmos DB, aby upewnić się, że zapytania są odpowiednio ograniczone do najmniejszej możliwej liczby partycji. Rozważmy na przykład następujące zapytanie LINQ, które dostarcza wszystkie trzy wartości klucza partycji w hierarchii:

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

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

Podczas wykonywania tego zapytania program EF Core wyodrębni wartości parametrów tenantId, userIdi sessionId przekazuje je do interfejsu API zapytania usługi Azure Cosmos DB jako wartość klucza partycji. Zobacz na przykład dzienniki z wykonywania powyższego zapytania:

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

Zwróć uwagę, że porównania kluczy partycji zostały usunięte z WHERE klauzuli i są zamiast tego używane jako klucz partycji do wydajnego wykonywania: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

Aby uzyskać więcej informacji, zobacz dokumentację dotyczącą wykonywania zapytań za pomocą kluczy partycji.

Znacznie ulepszone możliwości wykonywania zapytań LINQ

W programie EF 9.0 możliwości tłumaczenia LINQ dostawcy usługi Azure Cosmos DB zostały znacznie rozszerzone, a dostawca może teraz wykonywać znacznie więcej typów zapytań. Pełna lista ulepszeń zapytań jest zbyt długa, aby wyświetlić listę, ale oto główne najważniejsze informacje:

  • Pełna obsługa kolekcji pierwotnych platformy EF, umożliwiająca wykonywanie zapytań LINQ na kolekcjach np. kropek lub ciągów. Aby uzyskać więcej informacji, zobacz Co nowego w programie EF8: kolekcje pierwotne.
  • Obsługa dowolnych zapytań dotyczących kolekcji innych niż pierwotne.
  • Obecnie obsługiwane są wiele dodatkowych operatorów LINQ: indeksowanie kolekcji, Length/Count, ElementAt, Containsi wielu innych.
  • Obsługa operatorów agregacji, takich jak Count i Sum.
  • Dodatkowe tłumaczenia funkcji (zobacz dokumentację mapowań funkcji, aby uzyskać pełną listę obsługiwanych tłumaczeń):
    • Tłumaczenia elementów DateTime DateTimeOffset członkowskich i składników (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined a EF.Functions.CoalesceUndefined teraz zezwalaj na radzenie sobie z wartościami undefined .
    • string.ContainsStartsWith, a EndsWith teraz obsługują program StringComparison.OrdinalIgnoreCase.

Aby uzyskać pełną listę ulepszeń zapytań, zobacz ten problem:

Ulepszone modelowanie dostosowane do standardów JSON i Azure Cosmos DB

Program EF 9.0 mapuje dokumenty usługi Azure Cosmos DB na bardziej naturalny sposób na bazę danych dokumentów opartych na formacie JSON i pomaga współpracować z innymi systemami, które uzyskują dostęp do dokumentów. Mimo że wiąże się to ze zmianami powodującymi niezgodność, interfejsy API istnieją, które umożliwiają przywrócenie zachowania sprzed wersji 9.0 we wszystkich przypadkach.

Uproszczone id właściwości bez dyskryminujących

Najpierw poprzednie wersje programu EF wstawiły wartość dyskryminującą do właściwości JSON id , tworząc dokumenty takie jak następujące:

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

Zostało to zrobione, aby zezwolić na dokumenty różnych typów (np. blog i wpis) oraz tę samą wartość klucza (1099) do istnienia w tej samej partycji kontenera. Począwszy od programu EF 9.0, id właściwość zawiera tylko wartość klucza:

{
    "id": 1099,
    ...
}

Jest to bardziej naturalny sposób mapowania na dane JSON i ułatwia zewnętrznym narzędziom i systemom interakcję z dokumentami JSON generowanymi przez program EF; takie systemy zewnętrzne nie są ogólnie świadome wartości dyskryminacyjnych EF, które są domyślnie pochodzące z typów platformy .NET.

Pamiętaj, że jest to zmiana powodująca niezgodność, ponieważ program EF nie będzie już mógł wykonywać zapytań dotyczących istniejących dokumentów ze starym id formatem. Wprowadzono interfejs API, aby przywrócić poprzednie zachowanie, zobacz notatkę o zmianie powodującej niezgodność i dokumentację , aby uzyskać więcej informacji.

Nazwa właściwości dyskryminującej została zmieniona na $type

Domyślna właściwość dyskryminująca została wcześniej nazwana Discriminator. Program EF 9.0 zmienia wartość domyślną na $type:

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

Jest to zgodny z nowym standardem dla polimorfizmu JSON, co zapewnia lepszą współdziałanie z innymi narzędziami. Na przykład. Plik System.Text.Json platformy NET obsługuje również polimorfizm przy użyciu $type jako domyślnej nazwy właściwości dyskryminującej (docs).

Należy pamiętać, że jest to zmiana powodująca niezgodność, ponieważ program EF nie będzie już mógł wykonywać zapytań dotyczących istniejących dokumentów ze starą nazwą właściwości dyskryminującej. Zobacz notatkę dotyczącą zmiany powodującej niezgodność, aby uzyskać szczegółowe informacje na temat przywracania poprzedniego nazewnictwa.

Wyszukiwanie podobieństwa wektorów (wersja zapoznawcza)

Usługa Azure Cosmos DB oferuje teraz obsługę podglądu wyszukiwania podobieństwa wektorów. Wyszukiwanie wektorowe jest podstawową częścią niektórych typów aplikacji, w tym sztucznej inteligencji, wyszukiwania semantycznego i innych. Usługa Azure Cosmos DB umożliwia przechowywanie wektorów bezpośrednio w dokumentach wraz z resztą danych, co oznacza, że można wykonywać wszystkie zapytania względem pojedynczej bazy danych. Może to znacznie uprościć architekturę i usunąć potrzebę dodatkowego, dedykowanego rozwiązania bazy danych wektorów w stosie. Aby dowiedzieć się więcej na temat wyszukiwania wektorów usługi Azure Cosmos DB, zobacz dokumentację.

Po poprawnym skonfigurowaniu kontenera usługi Azure Cosmos DB korzystanie z wyszukiwania wektorowego za pośrednictwem platformy EF to prosta kwestia dodawania właściwości wektora i konfigurowania jej:

public class Blog
{
    ...

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

public class BloggingContext
{
    ...

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

Po wykonaniu EF.Functions.VectorDistance() tej czynności użyj funkcji w zapytaniach LINQ, aby wykonać wyszukiwanie podobieństwa wektorów:

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

Aby uzyskać więcej informacji, zobacz dokumentację dotyczącą wyszukiwania wektorów.

Pomoc techniczna obsługi stronicowania

Dostawca usługi Azure Cosmos DB umożliwia teraz stronicowanie wyników zapytań za pośrednictwem tokenów kontynuacji, co jest znacznie bardziej wydajne i ekonomiczne niż tradycyjne użycie Skip i Take:

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

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

Nowy ToPageAsync operator zwraca CosmosPageelement , który uwidacznia token kontynuacji, który może służyć do wydajnego wznawiania zapytania w późniejszym momencie, pobierając kolejne 10 elementów:

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

Aby uzyskać więcej informacji, zobacz sekcję dokumentacji dotyczącą stronicowania.

FromSql na potrzeby bezpieczniejszego wykonywania zapytań SQL

Dostawca usługi Azure Cosmos DB zezwolił na wykonywanie zapytań SQL za pośrednictwem polecenia FromSqlRaw. Jednak ten interfejs API może być podatny na ataki polegających na wstrzyknięciu kodu SQL, gdy dane dostarczone przez użytkownika są interpolowane lub łączone z bazą danych SQL. W programie EF 9.0 można teraz użyć nowej FromSql metody, która zawsze integruje sparametryzowane dane jako parametr poza programem SQL:

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

Aby uzyskać więcej informacji, zobacz sekcję dokumentacji dotyczącą stronicowania.

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 dla wszystkich operacji płaszczyzny danych. Jednak zestaw SDK usługi Azure Cosmos DB nie obsługuje kontroli dostępu opartej na rolach dla operacji płaszczyzny zarządzania w usłudze Azure Cosmos DB. Użyj interfejsu API usługi Azure Management zamiast z kontrolą dostępu opartą na rolach EnsureCreatedAsync .

Synchroniczne operacje we/wy są domyślnie blokowane

Usługa Azure Cosmos DB for NoSQL nie obsługuje synchronicznych (blokujących) interfejsów API z kodu aplikacji. Wcześniej program EF zamaskował to przez zablokowanie dla Ciebie wywołań asynchronicznych. Jednak oba te metody zachęcają do synchronicznego użycia operacji we/wy, co jest złą praktyką i może powodować zakleszczenia. W związku z tym, począwszy od programu EF 9, podczas próby synchronicznego dostępu jest zgłaszany wyjątek. Na przykład:

Synchroniczne operacje we/wy mogą być nadal używane 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 EF 11, więc zacznij aktualizować się tak szybko, aby korzystać z metod asynchronicznych, takich jak ToListAsync i SaveChangesAsync jak najszybciej!

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

Podobnie jak w przypadku każdej wersji, platforma EF9 zawiera dużą liczbę ulepszeń funkcji zapytań LINQ. Nowe zapytania można przetłumaczyć, a wiele tłumaczeń SQL w obsługiwanych scenariuszach zostało ulepszonych, co zapewnia lepszą wydajność i czytelność.

Liczba ulepszeń jest zbyt duża, aby wyświetlić je wszystkie tutaj. Poniżej wyróżniono niektóre z ważniejszych ulepszeń; Zobacz ten problem , aby uzyskać bardziej kompletną listę pracy wykonanej w wersji 9.0.

Chcielibyśmy zwrócić uwagę Andrea Canciani (@ranma42) na jego liczne, wysokiej jakości wkład w optymalizację języka SQL, który jest generowany przez ef Core!

Typy złożone: obsługa grupowania według i polecenia ExecuteUpdate

GroupBy

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]

ExecuteUpdate

Napiwek

Pokazany tutaj kod pochodzi z ExecuteUpdateSample.cs.

Podobnie w programie EF9 ExecuteUpdate ulepszono również akceptowanie 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 == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

Spowoduje to wygenerowanie bazy danych SQL, która aktualizuje każdą kolumnę zamapowaną na typ złożony:

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'

Wcześniej trzeba było ręcznie wyświetlić listę różnych właściwości typu złożonego w wywołaniu ExecuteUpdate .

Przycinanie niepotrzebnych elementów z bazy danych SQL

Wcześniej program EF czasami wyprodukował program SQL, który zawierał elementy, które nie były rzeczywiście potrzebne; w większości przypadków były one prawdopodobnie potrzebne na wcześniejszym etapie przetwarzania SQL i zostały pozostawione w tyle. Ef9 teraz przycina większość takich elementów, co powoduje bardziej kompaktowe i, w niektórych przypadkach, bardziej wydajne SQL.

Oczyszczanie tabeli

W pierwszym przykładzie język SQL wygenerowany przez platformę EF czasami zawierał numery JOIN do tabel, które nie były rzeczywiście potrzebne w zapytaniu. Rozważmy następujący model, który używa mapowania dziedziczenia tabeli na typ (TPT):

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

    public Customer Customer { get; set; }
}

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

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

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

public class BlogContext : DbContext
{
    ...

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

Jeśli następnie wykonamy następujące zapytanie, aby pobrać wszystkich klientów z co najmniej jednym zamówieniem:

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

Program EF8 wygenerował następujący kod SQL:

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

Zwróć uwagę, że zapytanie zawiera sprzężenie do DiscountedOrders tabeli, mimo że nie odwoływały się do niej żadne kolumny. Program EF9 generuje oczyszczony język SQL bez sprzężenia:

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

Oczyszczanie projekcji

Podobnie przyjrzyjmy się następującej kwerendzie:

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

Na platformie EF8 to zapytanie wygenerowało następujący kod SQL:

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

Należy pamiętać, że projekcja [o].[Id] nie jest potrzebna w podzapytaniu, ponieważ zewnętrzne wyrażenie SELECT po prostu zlicza wiersze. Zamiast tego program EF9 generuje następujące elementy:

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

... i projekcja jest pusta. Może to nie wydawać się zbyt duże, ale w niektórych przypadkach może znacznie uprościć proces SQL; Możesz przejrzeć niektóre zmiany języka SQL w testach , aby zobaczyć efekt.

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 GREATEST i LEAST zostały wprowadzone do baz 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.Max Math.Min są teraz tłumaczone dla usługi Azure SQL przy użyciu i GREATEST LEAST 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:

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 spowodowałoby zanieczyszczenie pamięci podręcznej zapytań wieloma zapytaniami, które różnią się tylko id wartościami. 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:

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

Metoda EF.Parameter

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":

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

Sparametryzowane kolekcje pierwotne

Program EF8 zmienił sposób tłumaczenia niektórych zapytań korzystających z kolekcji pierwotnych. Gdy zapytanie LINQ zawiera sparametryzowaną kolekcję pierwotną, program EF konwertuje jego zawartość na format JSON i przekazuje ją jako pojedynczą wartość parametru kwerendy:

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

Spowoduje to następujące tłumaczenie w programie SQL Server:

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

Umożliwia to posiadanie tego samego zapytania SQL dla różnych sparametryzowanych kolekcji (tylko zmiany wartości parametru), ale w niektórych sytuacjach może to prowadzić do problemów z wydajnością, ponieważ baza danych nie może optymalnie zaplanować zapytania. Metoda EF.Constant może służyć do przywracania poprzedniego tłumaczenia.

Następujące zapytanie używa EF.Constant tego elementu do tego efektu:

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

Wynikowy kod SQL jest następujący:

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

Ponadto program EF9 wprowadza TranslateParameterizedCollectionsToConstants opcję kontekstu, która może służyć do zapobiegania parametryzacji kolekcji pierwotnej dla wszystkich zapytań. Dodaliśmy również uzupełnienie TranslateParameterizedCollectionsToParameters , które wymusza jawne sparametryzację kolekcji pierwotnych (jest to zachowanie domyślne).

Napiwek

Metoda EF.Parameter zastępuje opcję kontekstu. Jeśli chcesz zapobiec parametryzacji kolekcji pierwotnych dla większości zapytań (ale nie wszystkich), możesz ustawić opcję TranslateParameterizedCollectionsToConstants kontekstu i użyć EF.Parameter dla zapytań lub poszczególnych zmiennych, które chcesz sparametryzować.

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 jedną rundę bazy danych:

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

Agregowanie funkcji w podzapytaniach i agregacjach w programie SQL Server

Program EF9 ulepsza tłumaczenie niektórych złożonych zapytań przy użyciu funkcji agregujących złożonych w podzapytaniach lub innych funkcjach agregujących. Poniżej przedstawiono przykład takiego zapytania:

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

Najpierw oblicza LatestPostRating dla każdego Post z nich, Select który wymaga podzapytania podczas tłumaczenia na język SQL. W dalszej części zapytania te wyniki są agregowane przy użyciu Average operacji. Wynikowy kod SQL wygląda następująco podczas uruchamiania w programie SQL Server:

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

W poprzednich wersjach program EF Core wygenerował nieprawidłowy kod SQL dla podobnych zapytań, próbując zastosować operację agregacji bezpośrednio w podzapytaniu. Nie jest to dozwolone w programie SQL Server i powoduje wyjątek. Ta sama zasada dotyczy zapytań używających agregacji w innej agregacji:

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

Uwaga

Ta zmiana nie ma wpływu na usługę Sqlite, która obsługuje agregacje w podzapytaniach (lub innych agregacjach) i nie obsługuje LATERAL JOIN (APPLY). Poniżej znajduje się baza danych SQL dla pierwszego zapytania uruchomionego w usłudze Sqlite:

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

Zapytania korzystające z liczby != 0 są zoptymalizowane

Napiwek

Pokazany tutaj kod pochodzi z QuerySample.cs.

W programie EF8 następujące zapytanie LINQ zostało przetłumaczone w celu użycia funkcji SQL COUNT :

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

Program EF9 generuje teraz bardziej wydajne tłumaczenie przy użyciu polecenia EXISTS:

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

Semantyka języka C# dla operacji porównania dla wartości dopuszczanych do wartości null

W przypadku porównania ef8 między elementami dopuszczanymi wartości null nie były wykonywane poprawnie w niektórych scenariuszach. W języku C#, jeśli jeden lub oba operandy mają wartość null, wynik operacji porównania ma wartość false; w przeciwnym razie porównywane są zawarte wartości operandów. W programie EF8 przetłumaczyliśmy porównania przy użyciu semantyki o wartości null bazy danych. Spowoduje to wygenerowanie wyników innych niż podobne zapytanie przy użyciu linQ to Objects. Co więcej, tworzymy różne wyniki podczas porównywania w filtrze a projekcji. Niektóre zapytania generują również różne wyniki między programem Sql Server i bazą danych Sqlite/Postgres.

Na przykład zapytanie:

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

wygeneruje następujący kod SQL:

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

filtruje jednostki, których NullableIntOne wartości lub NullableIntTwo są ustawione na wartość null.

W programie EF9 tworzymy:

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

Podobne porównanie wykonane w projekcji:

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

w wyniku następującego kodu SQL:

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

funkcja zwracana false dla jednostek, których NullableIntOne wartość lub NullableIntTwo jest ustawiona na wartość null (a nie true oczekiwano w języku C#). Uruchomienie tego samego scenariusza w wygenerowanych usługach Sqlite:

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

co powoduje Nullable object must have a value wyjątek, ponieważ tłumaczenie generuje null wartość w przypadkach, w których NullableIntOne lub NullableIntTwo mają wartość null.

Program EF9 obsługuje teraz te scenariusze prawidłowo, generując wyniki zgodne z linQ to Objects i u różnych dostawców.

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

Tłumaczenie operatorów Order LINQ i OrderDescending

Program EF9 umożliwia tłumaczenie uproszczonych operacji porządkowania LINQ (Order i OrderDescending). Te działania działają podobnie do OrderBy/OrderByDescending tych, ale nie wymagają argumentu. Zamiast tego stosują one domyślne porządkowanie — w przypadku jednostek oznacza to porządkowanie na podstawie wartości klucza podstawowego i innych typów porządkowanie na podstawie samych wartości.

Poniżej przedstawiono przykładowe zapytanie, które korzysta z uproszczonych operatorów porządkowania:

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

To zapytanie jest równoważne z następującymi elementami:

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

i tworzy następujący kod SQL:

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

Uwaga

Order metody i OrderDescending są obsługiwane tylko w przypadku kolekcji jednostek, typów złożonych lub skalarnych — nie będą one działać na bardziej złożonych projekcjach, np. kolekcjach typów anonimowych zawierających wiele właściwości.

To ulepszenie zostało wprowadzone przez absolwenta EF Team @bricelam. Dziękujemy!

Ulepszone tłumaczenie operatora negacji logicznej (!)

EF9 oferuje wiele optymalizacji dotyczących języka SQL CASE/WHEN, COALESCE, negacji i różnych innych konstrukcji; większość z nich została wniesiona przez Andrea Canciani (@ranma42) - wiele dzięki za te wszystkie! Poniżej szczegółowo omówimy tylko kilka z tych optymalizacji wokół negacji logicznej.

Przeanalizujmy następujące zapytanie:

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

W programie EF8 utworzymy następujący kod SQL:

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

W programie EF9 "wypychamy" NOT operację do porównania:

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

Innym przykładem, mającym zastosowanie do programu SQL Server, jest negowana operacja warunkowa.

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

W programie EF8 używanym do tworzenia zagnieżdżonych CASE bloków:

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

W programie EF9 usunęliśmy zagnieżdżanie:

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

W programie SQL Server podczas projekcji negowanej właściwości logicznej:

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

Program EF8 wygenerowałby CASE blok, ponieważ porównania nie mogą być wyświetlane w projekcji bezpośrednio w zapytaniach programu SQL Server:

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

W programie EF9 to tłumaczenie zostało uproszczone i teraz używa bitowego NOT (~):

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

Lepsza obsługa usług Azure SQL i Azure Synapse

Program EF9 umożliwia większą elastyczność podczas określania typu programu SQL Server, który jest przeznaczony. Zamiast konfigurować program EF za pomocą UseSqlServerprogramu , można teraz określić UseAzureSql wartość lub UseAzureSynapse. Dzięki temu program EF może tworzyć lepsze środowisko SQL podczas korzystania z usługi Azure SQL lub Azure Synapse. Platforma EF może korzystać z funkcji specyficznych dla bazy danych (np. dedykowanego typu dla formatu JSON w usłudze Azure SQL) lub obejść swoje ograniczenia (np. ESCAPE klauzula nie jest dostępna w przypadku korzystania z LIKE usługi Azure Synapse).

Inne ulepszenia zapytań

  • Obsługa zapytań dotyczących kolekcji pierwotnych wprowadzonych w programie EF8 została rozszerzona w celu obsługi wszystkich ICollection<T> typów. Należy pamiętać, że dotyczy to tylko kolekcji parametrów i wbudowanych — kolekcje pierwotne, które są częścią jednostek, są nadal ograniczone do tablic, list i w programie EF9 również tablice/listy tylko do odczytu.
  • Nowe ToHashSetAsync funkcje zwracające wyniki zapytania jako HashSet (#30033, dodane przez @wertzui).
  • TimeOnly.FromDateTime i FromTimeSpan są teraz tłumaczone na program SQL Server (#33678).
  • ToString wyliczenia są teraz tłumaczone (#33706, współautor przez @Danevandy99).
  • string.Join teraz przekłada się na CONCAT_WS w kontekście niegregowanym w programie SQL Server (#28899).
  • EF.Functions.PatIndex teraz przekłada się na funkcję programu SQL Server PATINDEX , która zwraca pozycję początkową pierwszego wystąpienia wzorca (#33702, @smnsht).
  • Sum i Average teraz działają dla miejsc dziesiętnych w sqlite (#33721, współautor przez @ranma42).
  • Poprawki i optymalizacje do string.StartsWith i EndsWith (#31482).
  • Convert.To* metody mogą teraz akceptować argument typu object (#33891, współautor przez @imangd).
  • Operacja exclusive-Or (XOR) jest teraz tłumaczona na program SQL Server (#34071, współautor przez @ranma42).
  • Optymalizacje dotyczące wartości null dla COLLATE operacji i AT TIME ZONE (#34263, współautor @ranma42).
  • Optymalizacje dla DISTINCT operacji ponad INi EXISTS set (#34381, współautor przez @ranma42).

Powyższe były tylko jednymi z ważniejszych ulepszeń zapytań w programie EF9; Zobacz ten problem , aby uzyskać bardziej kompletną listę.

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

Ochrona przed współbieżną migracją

Program EF9 wprowadza mechanizm blokowania w celu ochrony przed wieloma wykonywaniem migracji występujących jednocześnie, ponieważ może to spowodować pozostawienie bazy danych w stanie uszkodzonym. Nie dzieje się tak, gdy migracje są wdrażane w środowisku produkcyjnym przy użyciu zalecanych metod, ale mogą wystąpić, jeśli migracje są stosowane w czasie wykonywania przy użyciu DbContext.Database.Migrate() metody . Zalecamy stosowanie migracji we wdrożeniu, a nie w ramach uruchamiania aplikacji, ale może to spowodować bardziej skomplikowane architektury aplikacji (np. w przypadku korzystania z projektów aspirujących platformy .NET.

Uwaga

Jeśli używasz bazy danych Sqlite, zobacz potencjalne problemy związane z tą funkcją.

Ostrzegaj, gdy nie można uruchomić wielu operacji migracji wewnątrz transakcji

Większość operacji wykonywanych podczas migracji jest chroniona przez transakcję. Gwarantuje to, że jeśli z jakiegoś powodu migracja nie powiedzie się, baza danych nie zostanie uszkodzona. Jednak niektóre operacje nie są opakowane w transakcję (np. operacje w tabelach zoptymalizowanych pod kątem pamięci programu SQL Server lub operacje zmiany bazy danych, takie jak modyfikowanie sortowania bazy danych). Aby uniknąć uszkodzenia bazy danych w przypadku niepowodzenia migracji, zaleca się wykonanie tych operacji w izolacji przy użyciu oddzielnej migracji. Program EF9 wykrywa teraz scenariusz, w którym migracja zawiera wiele operacji, z których jedna nie może być opakowana w transakcję i wyświetla ostrzeżenie.

Ulepszone rozmieszczanie danych

Program EF9 wprowadził wygodny sposób wykonywania rozmieszczania danych, czyli wypełniania bazy danych przy użyciu danych początkowych. DbContextOptionsBuilder teraz zawiera UseSeeding metody i UseAsyncSeeding , które są wykonywane po zainicjowaniu obiektu DbContext (w ramach EnsureCreatedAsyncelementu ).

Uwaga

Jeśli aplikacja została uruchomiona wcześniej, baza danych może już zawierać przykładowe dane (które zostałyby dodane podczas pierwszej inicjalizacji kontekstu). W związku z tym należy sprawdzić, UseSeeding UseAsyncSeeding czy dane istnieją przed podjęciem próby wypełnienia bazy danych. Można to osiągnąć, wydając proste zapytanie EF.

Oto przykład użycia tych metod:

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

Więcej informacji można znaleźć tutaj.

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 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 między elementami 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.