Wat is er nieuw in EF Core 9?
EF Core 9 (EF9) is de volgende release na EF Core 8 en is gepland voor release in november 2024.
EF9 is beschikbaar als dagelijkse builds die alle nieuwste EF9-functies en API-aanpassingen bevatten. De voorbeelden hier maken gebruik van deze dagelijkse builds.
Tip
U kunt de voorbeelden uitvoeren en fouten opsporen door de voorbeeldcode te downloaden van GitHub. Elke sectie hieronder bevat koppelingen naar de broncode die specifiek is voor die sectie.
EF9 is gericht op .NET 8 en kan daarom worden gebruikt met .NET 8 (LTS) of .NET 9.
Tip
De Wat is er nieuw documentatie wordt bij elke preview bijgewerkt. Alle voorbeelden zijn ingesteld om de dagelijkse EF9-buildste gebruiken, die meestal verscheidene extra weken aan afgerond werk hebben vergeleken met de nieuwste preview. We raden het gebruik van de dagelijkse builds sterk aan bij het testen van nieuwe functies, zodat u uw tests niet uitvoert op verouderde bits.
Azure Cosmos DB voor NoSQL
EF 9.0 brengt aanzienlijke verbeteringen aan de EF Core-provider voor Azure Cosmos DB; belangrijke onderdelen van de provider zijn herschreven om nieuwe functionaliteit te bieden, nieuwe vormen van query's toe te staan en de provider beter af te stemmen op best practices van Azure Cosmos DB. Hieronder vindt u de belangrijkste verbeteringen op hoog niveau; voor een volledige lijst ziet dit epische probleem.
Waarschuwing
Als onderdeel van de verbeteringen in de provider moesten er een aantal ingrijpende wijzigingen worden aangebracht; als u een bestaande toepassing bijwerkt, lees dan de sectie ingrijpende wijzigingen zorgvuldig door.
Verbeteringen bij het uitvoeren van query's met partitiesleutels en document-id's
Elk document dat is opgeslagen in een Azure Cosmos DB-database heeft een unieke resource-id. Daarnaast kan elk document een 'partitiesleutel' bevatten waarmee de logische partitionering van gegevens wordt bepaald, zodat de database effectief kan worden geschaald. Meer informatie over het kiezen van partitiesleutels vindt u in Partitioneren en horizontaal schalen in Azure Cosmos DB.
In EF 9.0 is de Azure Cosmos DB-provider aanzienlijk beter bij het identificeren van vergelijkingen van partitiesleutels in uw LINQ-query's en het extraheren ervan om ervoor te zorgen dat uw query's alleen naar de relevante partitie worden verzonden; Dit kan de prestaties van uw query's aanzienlijk verbeteren en ru-kosten verlagen. Bijvoorbeeld:
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
In deze query herkent de provider automatisch de vergelijking op PartitionKey
; Als we de logboeken bekijken, zien we het volgende:
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")
Houd er rekening mee dat de WHERE
component geen PartitionKey
bevat: deze vergelijking is 'opgeheven' en wordt gebruikt om de query alleen uit te voeren op basis van de relevante partitie. In eerdere versies werd de vergelijking in veel situaties achtergelaten in de WHERE
-component, waardoor de query op alle partities wordt uitgevoerd, wat resulteert in hogere kosten en verminderde prestaties.
Als uw query ook een waarde biedt voor de eigenschap ID van het document en geen andere querybewerkingen bevat, kan de provider bovendien een extra optimalisatie toepassen:
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
In de logboeken ziet u het volgende voor deze query:
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
Hier wordt helemaal geen SQL-query verzonden. In plaats daarvan voert de provider een uiterst efficiënte puntlezing (ReadItem
-API) uit, waarmee het document direct wordt opgehaald op basis van de partitiesleutel en ID. Dit is het meest efficiënte en rendabele type leesbewerking dat u kunt uitvoeren in Azure Cosmos DB; raadpleegt u de Documentatie van Azure Cosmos DB voor meer informatie over puntleesbewerkingen.
Raadpleeg de query-documentatiepaginavoor meer informatie over het gebruik van partitiesleutels en puntlezingen
Hiërarchische partitioneringssleutels
Tip
De hier getoonde code is afkomstig van HierarchicalPartitionKeysSample.cs.
Azure Cosmos DB ondersteunt oorspronkelijk één partitiesleutel, maar heeft sindsdien uitgebreide partitioneringsmogelijkheden ter ondersteuning van subpartitionering via de specificatie van maximaal drie niveaus van hiërarchie in de partitiesleutel. EF Core 9 biedt volledige ondersteuning voor hiërarchische partitiesleutels, zodat u kunt profiteren van de betere prestaties en kostenbesparingen die aan deze functie zijn gekoppeld.
Partitiesleutels worden opgegeven met behulp van de api voor het bouwen van modellen, meestal in DbContext.OnModelCreating. Er moet een toegewezen eigenschap zijn in het entiteitstype voor elk niveau van de partitiesleutel. Denk bijvoorbeeld aan een UserSession
entiteitstype:
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!;
}
Met de volgende code wordt een partitiesleutel met drie niveaus opgegeven met behulp van de eigenschappen TenantId
, UserId
en SessionId
:
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
Fooi
Deze definitie van de partitiesleutel volgt het voorbeeld in Kies uw hiërarchische partitiesleutels uit de Documentatie van Azure Cosmos DB.
Let op hoe, vanaf EF Core 9, de eigenschappen van elk toegewezen type kunnen worden gebruikt in de partitiesleutel. Voor bool
en numerieke typen, zoals de eigenschap int SessionId
, wordt de waarde rechtstreeks in de partitiesleutel gebruikt. Andere typen, zoals de eigenschap Guid UserId
, worden automatisch geconverteerd naar tekenreeksen.
Bij het uitvoeren van query's extraheert EF automatisch de partitiesleutelwaarden uit query's en past deze toe op de Azure Cosmos DB-query-API om ervoor te zorgen dat de query's op de juiste wijze worden beperkt tot het kleinste aantal partities dat mogelijk is. Denk bijvoorbeeld aan de volgende LINQ-query die alle drie de partitiesleutelwaarden in de hiërarchie levert:
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();
Bij het uitvoeren van deze query extraheert EF Core de waarden van de tenantId
, userId
en sessionId
parameters en geeft deze door aan de Azure Cosmos DB-query-API als de partitiesleutelwaarde. Zie bijvoorbeeld de logboeken van het uitvoeren van de bovenstaande query:
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"))
Zie dat de partitiesleutelvergelijkingen zijn verwijderd uit de WHERE
clausule en in plaats daarvan worden gebruikt als de partitiesleutel voor een efficiënte uitvoering: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
.
Zie de documentatie over query's uitvoeren met partitiesleutelsvoor meer informatie.
Aanzienlijk verbeterde LINQ-querymogelijkheden
In EF 9.0 zijn de LINQ-vertaalmogelijkheden van de Azure Cosmos DB-provider aanzienlijk uitgebreid en kan de provider nu aanzienlijk meer querytypen uitvoeren. De volledige lijst met queryverbeteringen is te lang om weer te geven, maar hier volgen de belangrijkste hoogtepunten:
- Volledige ondersteuning voor primitieve verzamelingen van EF, zodat u LINQ-query's kunt uitvoeren op verzamelingen van bijvoorbeeld ints of tekenreeksen. Zie Wat is er nieuw in EF8: primitieve verzamelingen voor meer informatie.
- Ondersteuning voor willekeurige query's op niet-primitieve verzamelingen.
- Veel extra LINQ-operators worden nu ondersteund: indexeren in verzamelingen,
Length
/Count
,ElementAt
,Contains
en nog veel meer. - Ondersteuning voor aggregatie-operators zoals
Count
enSum
. - Aanvullende functievertalingen (zie de documentatie voor functietoewijzingen voor de volledige lijst met ondersteunde vertalingen):
- Vertalingen voor leden van
DateTime
enDateTimeOffset
onderdelen (DateTime.Year
,DateTimeOffset.Month
...). -
EF.Functions.IsDefined
enEF.Functions.CoalesceUndefined
kunnen nu omgaan metundefined
waarden. -
string.Contains
,StartsWith
enEndsWith
bieden nu ondersteuning voorStringComparison.OrdinalIgnoreCase
.
- Vertalingen voor leden van
Zie dit probleemvoor de volledige lijst met queryverbeteringen:
Verbeterde modellering die is afgestemd op Azure Cosmos DB- en JSON-standaarden
EF 9.0 past op een natuurlijkere manier aan bij Azure Cosmos DB-documenten voor een op JSON gebaseerde documentdatabase en helpt bij het interopereren met andere systemen die toegang hebben tot uw documenten. Hoewel dit belangrijke wijzigingen met zich meebrengt, bestaan ER API's die het mogelijk maken om in alle gevallen terug te keren naar het pre-9.0-gedrag.
Vereenvoudigde id
eigenschappen zonder onderscheiders
Ten eerste hebben eerdere versies van EF de discriminatorwaarde in de eigenschap JSON id
ingevoegd, waardoor documenten als de volgende documenten worden geproduceerd:
{
"id": "Blog|1099",
...
}
Dit is gedaan om documenten van verschillende typen (bijvoorbeeld Blog en Post) en dezelfde sleutelwaarde (1099) toe te staan binnen dezelfde containerpartitie. Vanaf EF 9.0 bevat de eigenschap id
alleen de sleutelwaarde:
{
"id": 1099,
...
}
Dit is een natuurlijkere manier om toe te wijzen aan JSON en maakt het eenvoudiger voor externe hulpprogramma's en systemen om te communiceren met door EF gegenereerde JSON-documenten; dergelijke externe systemen zijn over het algemeen niet op de hoogte van de EF-discriminatorwaarden, die standaard zijn afgeleid van .NET-typen.
Houd er rekening mee dat dit een belangrijke wijziging is, omdat EF geen query meer kan uitvoeren op bestaande documenten met de oude id
-indeling. Er is een API geïntroduceerd om terug te keren naar het vorige gedrag. Zie de belangrijke wijzigingsnotitie en de de documentatie voor meer informatie.
De naam van de eigenschap Discriminator is gewijzigd in $type
De standaarddiscriminatoreigenschap werd eerder Discriminator
genoemd. EF 9.0 wijzigt de standaardwaarde in $type
:
{
"id": 1099,
"$type": "Blog",
...
}
Dit volgt de opkomende standaard voor JSON-polymorfisme, waardoor betere interoperabiliteit met andere hulpprogramma's mogelijk is. Bijvoorbeeld. System.Text.Json van NET ondersteunt ook polymorfisme, waarbij gebruik wordt gemaakt van $type
als standaarddiscriminatieeigenschapsnaam (docs).
Houd er rekening mee dat dit een belangrijke wijziging is, omdat EF geen query meer kan uitvoeren op bestaande documenten met de oude naam van de discriminerende eigenschap. Zie de ingrijpende wijzigingsnotitie voor meer informatie over het herstellen van de vorige naamgeving.
Zoeken naar overeenkomsten tussen vectoren (preview)
Azure Cosmos DB biedt nu preview-ondersteuning voor zoeken naar overeenkomsten tussen vectoren. Vector zoeken is een fundamenteel onderdeel van sommige toepassingstypen, waaronder AI, semantische zoekopdrachten en andere. Met Azure Cosmos DB kunt u vectoren rechtstreeks in uw documenten opslaan naast de rest van uw gegevens, wat betekent dat u al uw query's op één database kunt uitvoeren. Dit kan uw architectuur aanzienlijk vereenvoudigen en de noodzaak voor een extra toegewezen vectordatabaseoplossing in uw stack verwijderen. Zie de documentatievoor meer informatie over het
Zodra uw Azure Cosmos DB-container correct is ingesteld, is het gebruik van vectorzoekopdrachten via EF een eenvoudige kwestie van het toevoegen van een vectoreigenschap en het configureren ervan:
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);
}
}
Zodra dat is gebeurd, gebruikt u de functie EF.Functions.VectorDistance()
in LINQ-query's om vector-overeenkomsten te zoeken:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
Zie de documentatie over vectorzoekopdrachtenvoor meer informatie.
Ondersteuning voor paginering
De Azure Cosmos DB-provider maakt het nu mogelijk om queryresultaten te pagineren via vervolgtokens, wat veel efficiënter en rendabeler is dan het traditionele gebruik van Skip
en 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
}
De nieuwe ToPageAsync
-operator retourneert een CosmosPage
, waarmee een vervolgtoken wordt weergegeven dat kan worden gebruikt om de query op een later moment efficiënt te hervatten, waarbij de volgende tien items worden opgehaald:
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
Voor meer informatie zie de documentatiesectie over paginering
FromSql voor veiligere SQL-query's
De Azure Cosmos DB-provider heeft SQL-query's via FromSqlRawtoegestaan. Deze API kan echter vatbaar zijn voor SQL-injectieaanvallen wanneer door de gebruiker verstrekte gegevens worden geïnterpoleerd of samengevoegd in de SQL. In EF 9.0 kunt u nu de nieuwe methode FromSql
gebruiken, die altijd geparameteriseerde gegevens integreert als een parameter buiten de SQL:
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
Voor meer informatie , zie de documentatiesectie over paginering.
Op rollen gebaseerde toegang
Azure Cosmos DB for NoSQL bevat een ingebouwd RBAC-systeem (op rollen gebaseerd toegangsbeheer). Dit wordt nu ondersteund door EF9 voor alle bewerkingen van het gegevensvlak. Azure Cosmos DB SDK biedt echter geen ondersteuning voor RBAC voor beheervlakbewerkingen in Azure Cosmos DB. Gebruik de Azure Management-API in plaats van EnsureCreatedAsync
met RBAC.
Synchrone I/O wordt nu standaard geblokkeerd
Azure Cosmos DB for NoSQL biedt geen ondersteuning voor synchrone (blokkerende) API's van toepassingscode. Eerder heeft EF dit gemaskeerd door u te blokkeren bij asynchrone aanroepen. Dit moedigt echter het gebruik van synchrone I/O aan, wat onwenselijk is, en kan patstellingen veroorzaken. Daarom wordt vanaf EF 9 een uitzondering gegenereerd wanneer synchrone toegang wordt geprobeerd. Bijvoorbeeld:
Synchrone I/O kan voorlopig nog steeds worden gebruikt door het waarschuwingsniveau op de juiste manier te configureren. Typ bijvoorbeeld in OnConfiguring
op uw DbContext
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
Houd er echter rekening mee dat we van plan zijn om de synchronisatieondersteuning in EF 11 volledig te verwijderen, dus begin met bijwerken om asynchrone methoden zoals ToListAsync
en SaveChangesAsync
zo snel mogelijk te gebruiken.
AOT en vooraf gecompileerde query's
Waarschuwing
NativeAOT en queryvoorcompilatie zijn zeer experimentele functies en zijn nog niet geschikt voor productiegebruik. De hieronder beschreven ondersteuning moet worden bekeken als infrastructuur voor de uiteindelijke functie, die waarschijnlijk wordt uitgebracht met EF 10. We moedigen u aan te experimenteren met de huidige ondersteuning en over uw ervaringen te rapporteren, maar raden af om EF NativeAOT-toepassingen in productie te implementeren.
EF 9.0 biedt initiële, experimentele ondersteuning voor .NET NativeAOT-, waardoor het publiceren van vooraf gecompileerde toepassingen die gebruikmaken van EF voor toegang tot databases mogelijk maakt. Ter ondersteuning van LINQ-query's in de nativeAOT-modus is EF afhankelijk van queryvoorcompilatie: dit mechanisme identificeert EF LINQ-query's statisch en genereert C# interceptors, die code bevatten om elke specifieke query uit te voeren. Dit kan de opstarttijd van uw toepassing aanzienlijk verminderen, omdat het zware werk van de verwerking en het compileren van uw LINQ-query's in SQL niet meer gebeurt wanneer uw toepassing wordt gestart. In plaats daarvan bevat de interceptor van elke query de voltooide SQL voor die query, evenals geoptimaliseerde code voor het materialiseren van databaseresultaten als .NET-objecten.
Bijvoorbeeld, gegeven een programma met de volgende EF-query:
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF genereert een C#-interceptor in uw project, die de uitvoering van de query overneemt. In plaats van de query te verwerken en deze te vertalen naar SQL telkens wanneer het programma wordt gestart, heeft de interceptor de SQL ingesloten (voor SQL Server in dit geval), zodat uw programma veel sneller kan worden opgestart:
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
Daarnaast bevat dezelfde interceptor code om uw .NET-object te materialiseren vanuit databaseresultaten:
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
Hiervoor wordt een andere nieuwe .NET-functie gebruikt: onveilige accessors, om gegevens uit de database in de privévelden van uw object te injecteren.
Als u geïnteresseerd bent in NativeAOT en graag wilt experimenteren met geavanceerde functies, kunt u dit proberen! Houd er rekening mee dat de functie als instabiel moet worden beschouwd en momenteel veel beperkingen heeft; we verwachten het te stabiliseren en het geschikter te maken voor productiegebruik in EF 10.
Zie de documentatiepagina van NativeAOT voor meer informatie.
LINQ en SQL vertaling
Net als bij elke release bevat EF9 een groot aantal verbeteringen in de MOGELIJKHEDEN van LINQ-query's. Nieuwe query's kunnen worden vertaald en veel SQL-vertalingen voor ondersteunde scenario's zijn verbeterd, voor zowel betere prestaties als leesbaarheid.
Het aantal verbeteringen is te geweldig om ze allemaal hier weer te geven. Hieronder worden enkele van de belangrijkste verbeteringen gemarkeerd; zie deze kwestie voor een vollediger overzicht van het werk dat is uitgevoerd in versie 9.0.
We willen Andrea Canciani (@ranma42) aanroepen voor zijn talrijke, hoogwaardige bijdragen aan het optimaliseren van de SQL die wordt gegenereerd door EF Core!
Complexe typen: ondersteuning voor GroupBy en ExecuteUpdate
GroupBy
Fooi
De hier getoonde code is afkomstig van ComplexTypesSample.cs.
EF9 ondersteunt groepering door een complex type instantie. Bijvoorbeeld:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF vertaalt dit als groepering door elk lid van het complexe type, dat overeenkomt met de semantiek van complexe typen als waardeobjecten. Bijvoorbeeld in Azure SQL:
SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]
ExecuteUpdate
Tip
De hier getoonde code is afkomstig van ExecuteUpdateSample.cs.
Op dezelfde manier is in EF9 ook ExecuteUpdate
verbeterd om complexe typen te accepteren. Elk lid van het complexe type moet echter expliciet worden opgegeven. Bijvoorbeeld:
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));
Hiermee wordt SQL gegenereerd waarmee elke kolom wordt bijgewerkt die is toegewezen aan het complexe type:
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'
Voorheen moest u de verschillende eigenschappen van het complexe type handmatig vermelden in uw ExecuteUpdate
aanroep.
Overbodige elementen uit SQL prunen
Voorheen maakte EF soms SQL die elementen bevatte die niet echt nodig waren; in de meeste gevallen waren deze mogelijk nodig in een eerdere fase van SQL-verwerking en bleven ze achter. EF9 verwijdert nu de meeste dergelijke elementen, wat resulteert in compacter en, in sommige gevallen, efficiëntere SQL.
Tabelsnoeien
Als eerste voorbeeld bevatte de SQL die door EF is gegenereerd, soms JOIN's voor tabellen die niet daadwerkelijk nodig waren in de query. Houd rekening met het volgende model, dat gebruikmaakt van
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();
}
}
Als we vervolgens de volgende query uitvoeren om alle klanten met ten minste één order op te halen:
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8 heeft de volgende SQL gegenereerd:
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])
Houd er rekening mee dat de query een join heeft met de DiscountedOrders
tabel, ook al is er niet naar kolommen verwezen. EF9 genereert een gesnoeide SQL zonder de join:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
Projectiesnoeien
Laten we ook de volgende query bekijken:
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
Op EF8 heeft deze query de volgende SQL gegenereerd:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
Houd er rekening mee dat de [o].[Id]
projectie niet nodig is in de subquery, omdat de buitenste SELECT-expressie simpelweg de rijen telt. EF9 genereert in plaats daarvan het volgende:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... en de projectie is leeg. Dit lijkt misschien niet veel, maar het kan de SQL in sommige gevallen aanzienlijk vereenvoudigen. Voel u vrij om door enkele van de SQL-wijzigingen van in de tests te bladeren om het effect te zien.
Vertalingen met BETREKKING tot GROOTSTE/MINST
Fooi
De hier getoonde code is afkomstig van LeastGreatestSample.cs.
Er zijn verschillende nieuwe vertalingen geïntroduceerd die gebruikmaken van de GREATEST
- en LEAST
SQL-functies.
Belangrijk
De GREATEST
- en LEAST
-functies zijn geïntroduceerd in SQL Server-/Azure SQL-databases in de versie 2022. Visual Studio 2022 installeert standaard SQL Server 2019. U wordt aangeraden SQL Server Developer Edition 2022 te installeren om deze nieuwe vertalingen uit te proberen in EF9.
Query's met Math.Max
of Math.Min
worden nu bijvoorbeeld vertaald voor Azure SQL met behulp van respectievelijk GREATEST
en LEAST
. Bijvoorbeeld:
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
Deze query wordt vertaald naar de volgende SQL bij gebruik van EF9 dat wordt uitgevoerd op 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
en Math.Max
kunnen ook worden gebruikt voor de waarden van een primitieve verzameling. Bijvoorbeeld:
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
Deze query wordt vertaald naar de volgende SQL bij gebruik van EF9 dat wordt uitgevoerd op 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
Ten slotte kunnen RelationalDbFunctionsExtensions.Least
en RelationalDbFunctionsExtensions.Greatest
worden gebruikt om de Least
of Greatest
functie rechtstreeks aan te roepen in SQL. Bijvoorbeeld:
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
Deze query wordt vertaald naar de volgende SQL bij gebruik van EF9 dat wordt uitgevoerd op 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]
Queryparameterisatie forceren of voorkomen
Tip
De hier getoonde code is afkomstig van QuerySample.cs.
Behalve in sommige speciale gevallen, parameteriseert EF Core variabelen die worden gebruikt in een LINQ-query, maar bevat constanten in de gegenereerde SQL. Denk bijvoorbeeld aan de volgende querymethode:
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
Dit wordt omgezet in de volgende SQL en parameters bij het gebruik van 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
U ziet dat EF een constante heeft gemaakt in de SQL voor '.NET-blog', omdat deze waarde niet wordt gewijzigd van query naar query. Door een constante te gebruiken, kan deze waarde door de database-engine worden onderzocht bij het maken van een queryplan, wat mogelijk resulteert in een efficiëntere query.
Aan de andere kant wordt de waarde van id
geparameteriseerd, omdat dezelfde query kan worden uitgevoerd met veel verschillende waarden voor id
. Het maken van een constante in dit geval zou leiden tot vervuiling van de querycache met veel query's die alleen verschillen in id
waarden. Dit is erg slecht voor de algehele prestaties van de database.
Over het algemeen moeten deze standaardwaarden niet worden gewijzigd. EF Core 8.0.2 introduceert echter een EF.Constant
methode die EF dwingt om een constante te gebruiken, zelfs als een parameter standaard wordt gebruikt. Bijvoorbeeld:
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
De vertaling bevat nu een constante voor de id
waarde:
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
De methode EF.Parameter
EF9 introduceert de EF.Parameter
methode om het tegenovergestelde te doen. Dwing ef dus af om een parameter te gebruiken, zelfs als de waarde een constante in code is. Bijvoorbeeld:
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
De vertaling bevat nu een parameter voor de tekenreeks ".NET Blog":
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
Geparameteriseerde primitieve verzamelingen
EF8 heeft de manier gewijzigd sommige query's die gebruikmaken van primitieve verzamelingen, worden vertaald. Wanneer een LINQ-query een geparameteriseerde primitieve verzameling bevat, converteert EF de inhoud ervan naar JSON en geeft deze door als één parameterwaarde voor de query:
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
Dit resulteert in de volgende vertaling in 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]
)
Hierdoor kan dezelfde SQL-query worden uitgevoerd voor verschillende geparameteriseerde verzamelingen (alleen de parameterwaarde wordt gewijzigd), maar in sommige gevallen kan dit leiden tot prestatieproblemen omdat de database niet optimaal kan plannen voor de query. De methode EF.Constant
kan worden gebruikt om terug te keren naar de vorige vertaling.
De volgende query maakt gebruik van EF.Constant
voor dat effect:
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
De resulterende SQL is als volgt:
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)
Bovendien introduceert EF9 TranslateParameterizedCollectionsToConstants
contextoptie dat kan worden gebruikt om primitieve verzamelingparameterisatie voor alle query's te voorkomen. We hebben ook een aanvulling toegevoegd TranslateParameterizedCollectionsToParameters
die parameterisatie van primitieve verzamelingen expliciet dwingt (dit is het standaardgedrag).
Tip
De methode EF.Parameter
overschrijft de contextoptie. Als u parameterisatie van primitieve verzamelingen voor de meeste query's (maar niet alle) wilt voorkomen, kunt u de contextoptie instellen TranslateParameterizedCollectionsToConstants
en EF.Parameter
gebruiken voor de query's of afzonderlijke variabelen die u wilt parameteriseren.
Ingelijnde niet-gerelateerde subquery's
Tip
De hier getoonde code is afkomstig van QuerySample.cs.
In EF8 kan een IQueryable waarnaar in een andere query wordt verwezen, worden uitgevoerd als een afzonderlijke roundtrip van de database. Denk bijvoorbeeld aan de volgende LINQ-query:
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = await dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArrayAsync();
In EF8 wordt de query voor dotnetPosts
uitgevoerd als één enkele transactie, en de uiteindelijke resultaten worden uitgevoerd met een tweede query. Bijvoorbeeld op SQL Server:
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
In EF9 wordt de IQueryable
in de dotnetPosts
ingevoegd, wat resulteert in een enkele database-rondreis.
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
Statistische functies via subquery's en aggregaties in SQL Server
EF9 verbetert de vertaling van enkele complexe query's met behulp van statistische functies die zijn samengesteld over subquery's of andere statistische functies. Hieronder ziet u een voorbeeld van een dergelijke query:
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();
Eerst berekent Select
LatestPostRating
voor elke Post
waarvoor een subquery is vereist bij het vertalen naar SQL. Verderop in de query worden deze resultaten samengevoegd met behulp van Average
bewerking. De resulterende SQL ziet er als volgt uit wanneer deze wordt uitgevoerd op 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]
In eerdere versies genereert EF Core ongeldige SQL voor vergelijkbare query's en probeert de statistische bewerking rechtstreeks via de subquery toe te passen. Dit is niet toegestaan op SQL Server en resulteert in een uitzondering. Hetzelfde principe is van toepassing op query's die gebruikmaken van een aggregatie over een andere aggregatie.
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();
Notitie
Deze wijziging heeft geen invloed op Sqlite, dat aggregaties ondersteunt via subquery's (of andere aggregaties) en biedt geen ondersteuning voor LATERAL JOIN
(APPLY
). Hieronder ziet u de SQL voor de eerste query die wordt uitgevoerd op 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"
Query's met aantal != 0 zijn geoptimaliseerd
Tip
De hier getoonde code is afkomstig van QuerySample.cs.
In EF8 is de volgende LINQ-query vertaald om de sql-COUNT
-functie te gebruiken:
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
EF9 genereert nu een efficiëntere vertaling met behulp van 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")
C#-semantiek voor vergelijkingsbewerkingen op null-waarden
In EF8-vergelijkingen tussen nullable elementen werden voor sommige scenario's niet correct uitgevoerd. Als in C# een of beide operanden null zijn, is het resultaat van een vergelijkingsbewerking onwaar; anders worden de ingesloten waarden van operanden vergeleken. In EF8 hebben we vergelijkingen vertaald met behulp van null-semantiek van databases. Hiermee worden resultaten geproduceerd die afwijken van een vergelijkbare query met behulp van LINQ naar objecten. Bovendien zouden we verschillende resultaten produceren wanneer de vergelijking werd uitgevoerd in filter versus projectie. Sommige query's produceren ook verschillende resultaten tussen Sql Server en Sqlite/Postgres.
Bijvoorbeeld de query:
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
genereert de volgende SQL:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
waarmee entiteiten worden gefilterd waarvan de NullableIntOne
of NullableIntTwo
zijn ingesteld op null.
In EF9 produceren we:
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)
Vergelijkbare vergelijking uitgevoerd in een projectie:
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
heeft geresulteerd in de volgende 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]
die false
retourneert voor entiteiten waarvan de NullableIntOne
of NullableIntTwo
zijn ingesteld op null (in plaats van true
verwacht in C#). Hetzelfde scenario uitvoeren op sqlite gegenereerd:
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
wat resulteert in Nullable object must have a value
uitzondering, omdat vertaling null
waarde produceert voor gevallen waarin NullableIntOne
of NullableIntTwo
null zijn.
EF9 verwerkt deze scenario's nu op de juiste manier, waardoor resultaten worden geproduceerd die consistent zijn met LINQ naar objecten en tussen verschillende providers.
Deze verbetering is bijgedragen door @ranma42. Veel dank!
Vertaling van Order
- en OrderDescending
LINQ-operators
EF9 maakt het mogelijk om vereenvoudigde LINQ-bestelbewerkingen (Order
en OrderDescending
) te vertalen. Deze werken vergelijkbaar met OrderBy
/OrderByDescending
, maar vereisen geen argument. In plaats daarvan passen ze standaardvolgorde toe: voor entiteiten betekent dit ordenen op basis van primaire-sleutelwaarden en voor andere typen, ordenen op basis van de waarden zelf.
Hieronder ziet u een voorbeeldquery die gebruikmaakt van de vereenvoudigde operatoren voor bestellen:
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();
Deze query is gelijk aan het volgende:
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();
en produceert de volgende 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]
Notitie
Order
en OrderDescending
methoden worden alleen ondersteund voor verzamelingen entiteiten, complexe typen of scalaire waarden. Ze werken niet aan complexere projecties, zoals verzamelingen van anonieme typen met meerdere eigenschappen.
Deze verbetering is bijgedragen door het EF Team alumnus @bricelam. Veel dank!
Verbeterde vertaling van logische negatieoperator (!)
EF9 biedt veel optimalisaties rond SQL CASE/WHEN
, COALESCE
, negatie en verschillende andere constructies; de meeste hiervan zijn bijgedragen door Andrea Canciani (@ranma42) - veel dank voor al deze! Hieronder worden slechts enkele van deze optimalisaties rond logische negatie beschreven.
Laten we de volgende query bekijken:
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
In EF8 produceren we de volgende SQL:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
In EF9 verplaatsen we de NOT
bewerking in de vergelijking.
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Een ander voorbeeld, dat van toepassing is op SQL Server, is een negated conditional operation.
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
In EF8 resulteerde dit in geneste CASE
blokken.
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]
In EF9 hebben we het nesten verwijderd:
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
Bij het projecteren van een ontkende booleigenschap in SQL Server:
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
EF8 genereert een CASE
blok omdat vergelijkingen niet rechtstreeks in sql Server-query's kunnen worden weergegeven in de projectie:
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]
In EF9 is deze vertaling vereenvoudigd en gebruikt nu bitwise NOT (~
):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Betere ondersteuning voor Azure SQL en Azure Synapse
EF9 biedt meer flexibiliteit bij het specificeren van het type SQL Server waarop wordt gericht. In plaats van EF te configureren met UseSqlServer
, kunt u nu UseAzureSql
of UseAzureSynapse
opgeven.
Hierdoor kan EF betere SQL produceren bij het gebruik van Azure SQL of Azure Synapse. EF kan profiteren van de databasespecifieke functies (bijvoorbeeld toegewezen type voor JSON in Azure SQL) of de beperkingen omzeilen (bijvoorbeeld ESCAPE
component is niet beschikbaar bij het gebruik van LIKE
op Azure Synapse).
Andere queryverbeteringen
- De ondersteuning voor het uitvoeren van query's op primitieve verzamelingen die zijn geïntroduceerd in EF8, is uitgebreid voor alle
ICollection<T>
typen. Dit geldt alleen voor parameters en inlineverzamelingen: primitieve verzamelingen die deel uitmaken van entiteiten, zijn nog steeds beperkt tot matrices, lijsten en in EF9, ook alleen-lezen matrices/lijsten. - Nieuwe
ToHashSetAsync
-functies om de resultaten van een query te retourneren als eenHashSet
(#30033, bijgedragen door @wertzui). -
TimeOnly.FromDateTime
enFromTimeSpan
worden nu vertaald in SQL Server (#33678). -
ToString
over enums is nu vertaald (#33706, bijgedragen door @Danevandy99). -
string.Join
wordt nu omgezet in CONCAT_WS in niet-geaggregeerde context op SQL Server (#28899). -
EF.Functions.PatIndex
wordt nu omgezet in de functie SQL ServerPATINDEX
, waarmee de beginpositie wordt geretourneerd van het eerste exemplaar van een patroon (#33702, @smnsht). -
Sum
enAverage
werken nu voor decimalen op SQLite (#33721, bijgedragen door @ranma42). - Oplossingen en optimalisaties voor
string.StartsWith
enEndsWith
(#31482). -
Convert.To*
methoden kunnen nu het argument van het typeobject
accepteren (#33891, bijgedragen door @imangd). - Exclusive-Or (XOR) wordt nu vertaald in SQL Server (#34071, bijgedragen door @ranma42).
- Optimalisaties rond null-functionaliteit voor
COLLATE
- enAT TIME ZONE
-bewerkingen (#34263, bijgedragen door @ranma42). - Optimalisaties voor
DISTINCT
ten opzichte vanIN
,EXISTS
en setbewerkingen (#34381, bijgedragen door @ranma42).
Het bovenstaande waren slechts enkele van de belangrijkste queryverbeteringen in EF9; zie dit probleem voor een volledigere vermelding.
Migraties
Beveiliging tegen gelijktijdige migraties
EF9 introduceert een vergrendelingsmechanisme ter bescherming tegen meerdere migratieuitvoeringen tegelijk, omdat de database hierdoor in een beschadigde status kan blijven. Dit gebeurt niet wanneer migraties worden geïmplementeerd in de productieomgeving met behulp van aanbevolen methoden, maar kunnen zich voordoen als migraties tijdens runtime worden toegepast met behulp van de DbContext.Database.MigrateAsync()
methode. We raden u aan migraties toe te passen bij de implementatie, in plaats van als onderdeel van het opstarten van toepassingen, maar dat kan leiden tot complexere toepassingsarchitecturen (bijvoorbeeld bij het gebruik van .NET Aspire-projecten).
Notitie
Als u de SQLite-database gebruikt, bekijk de mogelijke problemen die gerelateerd zijn aan deze functie.
Waarschuwen wanneer meerdere migratiebewerkingen niet binnen een transactie kunnen worden uitgevoerd
De meeste bewerkingen die tijdens migraties worden uitgevoerd, worden beveiligd door een transactie. Dit zorgt ervoor dat als de migratie om een of andere reden mislukt, de database niet in een beschadigde status terechtkomt. Sommige bewerkingen worden echter niet verpakt in een transactie (bijvoorbeeld bewerkingen in tabellen die zijn geoptimaliseerd voor SQL Server-geheugen, of bewerkingen voor het wijzigen van databases, zoals het wijzigen van de databasesortering). Als u wilt voorkomen dat de database beschadigd raakt in geval van een migratiefout, wordt u aangeraden deze bewerkingen geïsoleerd uit te voeren met behulp van een afzonderlijke migratie. EF9 detecteert nu een scenario wanneer een migratie meerdere bewerkingen bevat, waarvan een migratie niet kan worden verpakt in een transactie en een waarschuwing geeft.
Verbeterde seeding van gegevens
EF9 heeft een handige manier geïntroduceerd om gegevens in te voeren, waarmee de database wordt gevuld met begingegevens.
DbContextOptionsBuilder
bevat nu UseSeeding
en UseAsyncSeeding
methoden die worden uitgevoerd wanneer dbContext wordt geïnitialiseerd (als onderdeel van EnsureCreatedAsync
).
Notitie
Als de toepassing eerder is uitgevoerd, bevat de database mogelijk al de voorbeeldgegevens (die tijdens de eerste initialisatie van de context zouden zijn toegevoegd). Als zodanig moet UseSeeding
UseAsyncSeeding
controleren of er gegevens bestaan voordat de database wordt gevuld. Dit kan worden bereikt door een eenvoudige EF-query uit te geven.
Hier volgt een voorbeeld van hoe deze methoden kunnen worden gebruikt:
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);
}
});
Meer informatie vindt u hier .
Andere migratieverbeteringen
- Wanneer u een bestaande tabel wijzigt in een tijdelijke SQL Server-tabel, is de grootte van de migratiecode aanzienlijk verminderd.
Modelbouw
Automatisch gecompileerde modellen
Tip
De code die hier wordt weergegeven, is afkomstig uit het voorbeeld NewInEFCore9.CompiledModels.
Gecompileerde modellen kunnen de opstarttijd voor toepassingen met grote modellen verbeteren, dat wil zeggen modellen met aantallen entiteitstypen in de honderden of duizenden. In eerdere versies van EF Core moest een gecompileerd model handmatig worden gegenereerd met behulp van de opdrachtregel. Bijvoorbeeld:
dotnet ef dbcontext optimize
Nadat u de opdracht hebt uitgevoerd, moet er een regel zoals .UseModel(MyCompiledModels.BlogsContextModel.Instance)
worden toegevoegd aan OnConfiguring
om EF Core te informeren dat het gecompileerde model moet worden gebruikt.
Vanaf EF9 is deze .UseModel
regel niet meer nodig wanneer het DbContext
type van de toepassing zich in hetzelfde project/assembly bevindt als het gecompileerde model. In plaats daarvan wordt het gecompileerde model gedetecteerd en automatisch gebruikt. Dit kan worden gezien door EF te laten loggen wanneer het model wordt gebouwd. Bij het uitvoeren van een eenvoudige toepassing toont EF het opbouwen van het model wanneer de toepassing start.
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
De uitvoer van het uitvoeren van dotnet ef dbcontext optimize
in het modelproject is:
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>
U ziet dat de logboekuitvoer aangeeft dat het model is gebouwd bij het uitvoeren van de opdracht. Als we de toepassing nu opnieuw uitvoeren na het opnieuw opbouwen, maar zonder codewijzigingen aan te brengen, is de uitvoer:
Starting application...
Model loaded with 2 entity types.
U ziet dat het model niet is gebouwd bij het starten van de toepassing omdat het gecompileerde model automatisch is gedetecteerd en gebruikt.
MSBuild-integratie
Met de bovenstaande benadering moet het gecompileerde model nog steeds handmatig opnieuw worden gegenereerd wanneer de entiteitstypen of DbContext
configuratie wordt gewijzigd. EF9 wordt echter geleverd met een MSBuild-taakpakket dat het gecompileerde model automatisch kan bijwerken wanneer het modelproject wordt gebouwd! Installeer het Microsoft.EntityFrameworkCore.Tasks NuGet-pakket om aan de slag te gaan. Bijvoorbeeld:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
Fooi
Gebruik de pakketversie in de bovenstaande opdracht die overeenkomt met de versie van EF Core die u gebruikt.
Schakel vervolgens de integratie in door de eigenschappen EFOptimizeContext
en EFScaffoldModelStage
in uw .csproj
-bestand in te stellen. Bijvoorbeeld:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>
Als we nu het project bouwen, zien we logboekregistratie tijdens de build die aangeeft dat het gecompileerde model wordt gebouwd:
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
En het uitvoeren van de toepassing laat zien dat het gecompileerde model is gedetecteerd en daarom is het model niet opnieuw gebouwd:
Starting application...
Model loaded with 2 entity types.
Wanneer het model wordt gewijzigd, wordt het gecompileerde model nu automatisch opnieuw opgebouwd zodra het project is gebouwd.
Zie MSBuild-integratievoor meer informatie.
Niet-bewerkbare primitieve verzamelingen
Tip
De hier getoonde code is afkomstig van PrimitiveCollectionsSample.cs.
EF8 heeft ondersteuning geïntroduceerd voor toewijzingsmatrixen en veranderlijke lijsten met primitieve typen. In EF9 is dit uitgebreid om verzamelingen en lijsten die alleen-lezen zijn op te nemen. EF9 ondersteunt met name verzamelingen die zijn getypt als IReadOnlyList
, IReadOnlyCollection
of ReadOnlyCollection
. In de volgende code wordt DaysVisited
bijvoorbeeld per conventie toegewezen als een primitieve verzameling datums:
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
De alleen-lezen verzameling kan indien gewenst worden ondersteund door een normale, veranderbare verzameling. In de volgende code kan DaysVisited
bijvoorbeeld worden gemapt als een primitieve verzameling data, terwijl de code in de klasse nog steeds de onderliggende lijst kan manipuleren.
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;
}
Deze verzamelingen kunnen vervolgens op de normale manier worden gebruikt in query's. Bijvoorbeeld deze LINQ-query:
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();
Dit wordt omgezet in de volgende SQL op 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"
Vulfactor voor sleutels en indexen opgeven
Tip
De hier getoonde code is afkomstig van ModelBuildingSample.cs.
EF9 ondersteunt de specificatie van de SQL Server fill-factor bij gebruik van EF Core Migrations voor het maken van sleutels en indexen. In de SQL Server-documenten 'Wanneer een index wordt gemaakt of opnieuw opgebouwd, bepaalt de waarde van de vulfactor het percentage ruimte op elke pagina op bladniveau die moet worden gevuld met gegevens, waarbij de rest op elke pagina wordt opgeslagen als vrije ruimte voor toekomstige groei.'
De fill-factor kan worden ingesteld voor afzonderlijke of samengestelde primaire en alternatieve sleutels en indexen. Bijvoorbeeld:
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);
Wanneer deze op bestaande tabellen worden toegepast, worden de tabellen aangepast aan de fill-factor zoals vastgelegd door de beperking.
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);
Deze verbetering is bijgedragen door @deano-hunter. Veel dank!
Bestaande modelbouwconventies uitbreidbaar maken
Tip
De hier getoonde code is afkomstig van CustomConventionsSample.cs.
Openbare modelbouwconventies voor toepassingen zijn geïntroduceerd in EF7. In EF9 hebben we het eenvoudiger gemaakt om een aantal van de bestaande conventies uit te breiden. Bijvoorbeeld, de code om eigenschappen per attribuut toe te wijzen in EF7 is dit:
public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
public override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);
public override void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}
private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&& Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
foreach (var property in clrType.GetRuntimeProperties()
.Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
{
yield return property;
}
foreach (var property in clrType.GetRuntimeFields())
{
yield return property;
}
}
}
}
In EF9 kan dit worden vereenvoudigd tot het volgende:
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;
}
}
ApplyConfigurationsFromAssembly bijwerken om niet-openbare constructors aan te roepen
In eerdere versies van EF Core heeft de ApplyConfigurationsFromAssembly
methode alleen configuratietypen geïnstantieerd met een openbare, parameterloze constructors. In EF9 hebben we beide de foutberichten verbeterd die worden gegenereerd wanneer dit mislukten ook instantiëring door niet-openbare constructor is ingeschakeld. Dit is handig bij het co-loceren van configuratie in een privé geneste klasse die nooit door toepassingscode mag worden geïnstantieerd. Bijvoorbeeld:
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);
}
}
}
Terzijde, sommige mensen denken dat dit patroon een gruwel is omdat het het entiteitstype koppelt aan de configuratie. Andere personen vinden het erg nuttig omdat de configuratie wordt gekoppeld aan het entiteitstype. Laten we dit hier niet bespreken. :-)
SQL Server HierarchyId
Tip
De hier getoonde code is afkomstig van HierarchyIdSample.cs.
Syntaxsuiker voor HierarchyId-padgeneratie
Eersteklas ondersteuning voor het type SQL Server HierarchyId
is toegevoegd in EF8. In EF9 is een hulpmethode toegevoegd om het gemakkelijker te maken nieuwe onderliggende knooppunten in de boomstructuur te creëren. De volgende code voert een query uit op een bestaande entiteit met een eigenschap HierarchyId
.
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
Deze HierarchyId
-eigenschap kan vervolgens worden gebruikt om subknooppunten te maken zonder expliciet de tekenreeks te manipuleren. Bijvoorbeeld:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
Als daisy
een HierarchyId
van /4/1/3/1/
heeft, krijgt child1
de HierarchyId
"/4/1/1/1/1/1/" en krijgt child2
de HierarchyId
"/4/1/1/1/2/".
Als u een knooppunt tussen deze twee onderliggende elementen wilt maken, kunt u een extra subniveau gebruiken. Bijvoorbeeld:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
Hiermee maakt u een knooppunt met een HierarchyId
van /4/1/3/1/1.5/
, waarbij het tussen child1
en child2
wordt.
Deze verbetering is bijgedragen door @Rezakazemi890. Veel dank!
Gereedschap
Minder herbouwingen
Het dotnet ef
opdrachtregelprogramma bouwt uw project standaard voordat u het hulpprogramma uitvoert. Dit komt doordat het overslaan van heropbouw voordat het hulpprogramma wordt uitgevoerd, een veelvoorkomende bron van verwarring is wanneer dingen niet werken. Ervaren ontwikkelaars kunnen de --no-build
-optie gebruiken om deze build te voorkomen, wat mogelijk traag is. Zelfs de optie --no-build
kan er echter toe leiden dat het project opnieuw wordt gebouwd wanneer het de volgende keer buiten de EF-hulpprogramma's wordt gebouwd.
We zijn van mening dat een bijdrage van de community van @Suchiman dit heeft opgelost. We zijn er echter ook van bewust dat aanpassingen rond MSBuild-gedrag de neiging hebben om onbedoelde gevolgen te hebben, dus we vragen mensen zoals u dit uit te proberen en terug te melden over eventuele negatieve ervaringen die u hebt.