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 .NET 9.
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 . PartitionKey
Jeś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 PartitionKey
elementu : 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 TenantId
przy użyciu właściwości , UserId
i 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
, userId
i 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
,Contains
i wielu innych. - Obsługa operatorów agregacji, takich jak
Count
iSum
. - 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
aEF.Functions.CoalesceUndefined
teraz zezwalaj na radzenie sobie z wartościamiundefined
.string.Contains
StartsWith
, aEndsWith
teraz obsługują programStringComparison.OrdinalIgnoreCase
.
- Tłumaczenia elementów
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 CosmosPage
element , 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
Ostrzeżenie
Prekompilacja nativeAOT i zapytań są wysoce eksperymentalnymi funkcjami i nie są jeszcze odpowiednie do użytku produkcyjnego. Pomoc techniczna opisana poniżej powinna być postrzegana jako infrastruktura w kierunku ostatniej funkcji, która prawdopodobnie zostanie wydana z programem EF 10. Zachęcamy do eksperymentowania z bieżącą pomocą techniczną i raportem na temat Twoich środowisk, ale zalecamy wdrożenie aplikacji EF NativeAOT w środowisku produkcyjnym.
Program EF 9.0 zapewnia początkową, eksperymentalną obsługę platformy .NET NativeAOT, umożliwiając publikowanie wcześniej skompilowanych aplikacji, które korzystają z programu EF do uzyskiwania dostępu do baz danych. Aby obsługiwać zapytania LINQ w trybie NativeAOT, platforma EF opiera się na prekompilacji zapytań: ten mechanizm statycznie identyfikuje zapytania EF LINQ i generuje przechwytniki języka C#, które zawierają kod do wykonania poszczególnych zapytań. Może to znacząco zmniejszyć czas uruchamiania aplikacji, ponieważ duże obciążenie przetwarzania i kompilowania zapytań LINQ w języku SQL nie odbywa się już za każdym razem, gdy aplikacja się uruchamia. Zamiast tego przechwytywanie każdego zapytania zawiera sfinalizowany kod SQL dla tego zapytania, a także zoptymalizowany kod w celu materializowania wyników bazy danych jako obiektów platformy .NET.
Na przykład biorąc pod uwagę program z następującym zapytaniem EF:
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
Program EF wygeneruje przechwytujący język C# w projekcie, który przejmie wykonywanie zapytania. Zamiast przetwarzać zapytanie i tłumaczyć je na język SQL za każdym razem, gdy program się uruchamia, przechwytywanie ma osadzony w nim kod SQL (w tym przypadku program SQL Server), co pozwala programowi uruchomić się znacznie szybciej:
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
Ponadto ten sam przechwytujący zawiera kod umożliwiający materializowanie obiektu platformy .NET z wyników bazy danych:
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
Używa to innej nowej funkcji platformy .NET — niebezpiecznych metod dostępu do wstrzykiwania danych z bazy danych do prywatnych pól obiektu.
Jeśli interesuje Cię nativeAOT i chcesz eksperymentować z najnowocześniejszymi funkcjami, wypróbuj to! Należy pamiętać, że funkcja powinna być uważana za niestabilną i obecnie ma wiele ograniczeń; Spodziewamy się ustabilizować go i uczynić go bardziej odpowiednim do użycia produkcyjnego w EF 10.
Aby uzyskać więcej informacji, zobacz stronę dokumentacji nativeAOT.
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ą UseSqlServer
programu , 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 jakoHashSet
(#30033, dodane przez @wertzui). TimeOnly.FromDateTime
iFromTimeSpan
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 ServerPATINDEX
, która zwraca pozycję początkową pierwszego wystąpienia wzorca (#33702, @smnsht).Sum
iAverage
teraz działają dla miejsc dziesiętnych w sqlite (#33721, współautor przez @ranma42).- Poprawki i optymalizacje do
string.StartsWith
iEndsWith
(#31482). Convert.To*
metody mogą teraz akceptować argument typuobject
(#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 iAT TIME ZONE
(#34263, współautor @ranma42). - Optymalizacje dla
DISTINCT
operacji ponadIN
iEXISTS
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
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 EnsureCreatedAsync
elementu ).
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.
Inne ulepszenia migracji
- Podczas zmiany istniejącej tabeli na tabelę czasową programu SQL Server rozmiar kodu migracji został znacznie zmniejszony.
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 pakietem zadań MSBuild, 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
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ści i EFScaffoldModelStage
w .csproj
pliku. Na przykład:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</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
--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.
Aby uzyskać więcej informacji, zobacz Integracja programu MSBuild.
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
, IReadOnlyCollection
lub 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.