Dela via


Nyheter i EF Core 9

EF Core 9 (EF9) är nästa version efter EF Core 8 och är planerad att släppas i november 2024.

EF9 är tillgängligt som dagliga versioner som innehåller alla de senaste EF9-funktionerna och API-justeringarna. Exemplen här använder dessa dagliga versioner.

Tips

Du kan köra och felsöka i exemplen genom att ladda ned exempelkoden från GitHub. Varje avsnitt nedan länkar till källkoden som är specifik för det avsnittet.

EF9 riktar in sig på .NET 8 och kan därför användas med antingen .NET 8 (LTS) eller .NET 9.

Tips

Dokumenten Nyheter uppdateras för varje förhandsversion. Alla exempel är konfigurerade för att använda EF9 dagliga versioner, som vanligtvis har flera ytterligare veckors slutfört arbete jämfört med den senaste förhandsversionen. Vi rekommenderar starkt att du använder de dagliga versionerna när du testar nya funktioner så att du inte testar mot inaktuella bitar.

Azure Cosmos DB för NoSQL

EF 9.0 ger betydande förbättringar av EF Core-providern för Azure Cosmos DB. betydande delar av providern har skrivits om för att tillhandahålla nya funktioner, tillåta nya former av frågor och bättre anpassa providern till bästa praxis för Azure Cosmos DB. De viktigaste förbättringarna på hög nivå visas nedan. för en fullständig lista se det här episka problemet.

Varning

Som en del av förbättringarna som införs i leverantören, måste ett antal betydande icke-bakåtkompatibla ändringar göras; om du uppgraderar ett befintligt program, läs avsnittet icke-bakåtkompatibla ändringar noggrant.

Förbättringar av frågeställningar med partitionsnycklar och dokument-ID:n

Varje dokument som lagras i en Azure Cosmos DB-databas har ett unikt resurs-ID. Dessutom kan varje dokument innehålla en "partitionsnyckel" som avgör den logiska partitioneringen av data så att databasen kan skalas effektivt. Mer information om hur du väljer partitionsnycklar finns i Partitionering och horisontell skalning i Azure Cosmos DB.

I EF 9.0 är Azure Cosmos DB-providern betydligt bättre på att identifiera partitionsnyckeljämförelser i dina LINQ-frågor och extrahera dem för att säkerställa att dina frågor endast skickas till relevant partition. Detta kan avsevärt förbättra prestandan för dina frågor och minska RU-avgifterna. Till exempel:

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

I den här frågan identifierar providern automatiskt jämförelsen på PartitionKey; Om vi undersöker loggarna ser vi följande:

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

Observera att WHERE-satsen inte innehåller PartitionKey: den jämförelsen har "lyfts" ut och används endast för att köra frågan mot den relevanta partitionen. I tidigare versioner lämnades jämförelsen i WHERE-satsen i många situationer, vilket gjorde att frågan kördes mot alla partitioner och resulterade i ökade kostnader och lägre prestanda.

Om frågan dessutom ger ett värde för dokumentets ID-egenskap och inte innehåller några andra frågeåtgärder, kan providern tillämpa ytterligare en optimering:

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

Loggarna visar följande för den här frågan:

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

Här skickas ingen SQL-fråga alls. I stället utför providern en extremt effektiv läspunkt (ReadItem API) som direkt hämtar dokumentet givet partitionsnyckeln och ID:t. Det här är den mest effektiva och kostnadseffektiva typen av läsning som du kan utföra i Azure Cosmos DB. finns i Azure Cosmos DB-dokumentationen för mer information om punktläsningar.

För att lära dig mer om frågeställning med partitionsnycklar och punktläsningar kan du se dokumentationssidan för sökfrågor.

Hierarkiska partitionsnycklar

Tips

Koden som visas här kommer från HierarchicalPartitionKeysSample.cs.

Azure Cosmos DB stödde ursprungligen en enda partitionsnyckel, men har sedan dess utökat partitioneringsfunktionerna för att även stödja underpartitionering via specifikationen av upp till tre hierarkinivåer i partitionsnyckeln. EF Core 9 ger fullständigt stöd för hierarkiska partitionsnycklar, så att du kan dra nytta av bättre prestanda och kostnadsbesparingar som är associerade med den här funktionen.

Partitionsnycklar anges med hjälp av modellskapande-API:et, vanligtvis i DbContext.OnModelCreating. Det måste finnas en mappad egenskap i entitetstypen för varje nivå av partitionsnyckeln. Tänk dig till exempel en UserSession entitetstyp:

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

Följande kod anger en partitionsnyckel på tre nivåer med egenskaperna TenantId, UserIdoch SessionId:

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

Tips

Den här partitionsnyckeldefinitionen följer exemplet i Välj dina hierarkiska partitionsnycklar i Azure Cosmos DB-dokumentationen.

Observera att från och med EF Core 9 kan egenskaper av alla mappade typer användas i partitionsnyckeln. För bool och numeriska typer, som egenskapen int SessionId, används värdet direkt i partitionsnyckeln. Andra typer, till exempel egenskapen Guid UserId, konverteras automatiskt till strängar.

Vid frågor extraherar EF automatiskt partitionsnyckelvärdena från frågor och tillämpar dem på Azure Cosmos DB-fråge-API:et för att säkerställa att frågorna begränsas på lämpligt sätt till så få partitioner som möjligt. Tänk dig till exempel följande LINQ-fråga som tillhandahåller alla tre partitionsnyckelvärdena i hierarkin:

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

När du kör den här frågan extraherar EF Core värdena för parametrarna tenantId, userIdoch sessionId och skickar dem till Azure Cosmos DB-fråge-API:et som partitionsnyckelvärde. Se till exempel loggarna från att köra frågan ovan:

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

Observera att partitionsnyckeljämförelserna har tagits bort från WHERE-satsen och i stället används som partitionsnyckel för effektiv körning: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

För mer information, se dokumentationen om -frågningar med partitionsnycklar.

Avsevärt förbättrade LINQ-frågefunktioner

I EF 9.0 har LINQ-översättningsfunktionerna för Azure Cosmos DB-providern utökats avsevärt och providern kan nu köra betydligt fler frågetyper. Den fullständiga listan med frågeförbättringar är för lång för att kunna listas, men här är de viktigaste höjdpunkterna:

  • Fullständigt stöd för EF:s primitiva samlingar, så att du kan köra LINQ-frågor på samlingar med t.ex. ints eller strängar. För mer information, se Nyheter i EF8: primitiva samlingar.
  • Stöd för godtyckliga frågor över icke-primitiva samlingar.
  • Många ytterligare LINQ-operatorer stöds nu: indexering i samlingar, Length/Count, ElementAt, Containsoch många andra.
  • Stöd för aggregerade operatorer som Count och Sum.
  • Ytterligare funktionsöversättningar (se dokumentationen om -funktionsmappningar för den fullständiga listan över översättningar som stöds):
    • Översättningar för DateTime- och DateTimeOffset komponentmedlemmar (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined och EF.Functions.CoalesceUndefined tillåter nu hantering av undefined värden.
    • string.Containsstöder StartsWith och EndsWith nu StringComparison.OrdinalIgnoreCase.

Den fullständiga listan över frågeförbättringar finns i den här utgåvan:

Förbättrad modellering anpassad efter Azure Cosmos DB- och JSON-standarder

EF 9.0 mappar till Azure Cosmos DB-dokument på ett mer naturligt sätt för en JSON-baserad dokumentdatabas och hjälper till att samverka med andra system som har åtkomst till dina dokument. Även om detta medför större förändringar finns API:er som möjliggör att återgå till beteendet före 9.0 i samtliga fall.

Förenklade id egenskaper utan särskiljare

För det första infogade tidigare versioner av EF det diskriminerande värdet i egenskapen JSON id och skapade dokument som följande:

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

Detta gjordes för att dokument av olika typer (t.ex. blogg och inlägg) och samma nyckelvärde (1099) ska finnas inom samma containerpartition. Från och med EF 9.0 innehåller egenskapen id endast nyckelvärdet:

{
    "id": 1099,
    ...
}

Detta är ett mer naturligt sätt att mappa till JSON och gör det enklare för externa verktyg och system att interagera med EF-genererade JSON-dokument. sådana externa system är inte allmänt medvetna om ef-diskriminerande värden, som som standard härleds från .NET-typer.

Observera att det här är en brytande förändring eftersom EF inte längre kan fråga befintliga dokument med det gamla id-formatet. Ett API har introducerats för att återgå till det tidigare beteendet, se brytningsändringsnot och dokumentationen för mer detaljerad information.

Diskriminerande egendom omdöpt till $type

Standarddiskriminatoregenskapen hette tidigare Discriminator. EF 9.0 ändrar standardvärdet till $type:

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

Detta följer den nya standarden för JSON-polymorfism, vilket ger bättre samverkan med andra verktyg. Till exempel. NET:s System.Text.Json stöder också polymorfism med $type som standardnamn för diskriminerande egenskap (dokument).

Observera att detta är en icke-bakåtkompatibel ändring eftersom EF inte längre kommer att kunna köra frågor mot befintliga dokument med det gamla egenskapsnamnet för diskriminerande. För mer information om hur du återgår till föregående namngivning, se brytningsändringsanteckningen .

Vektorlikhetssökning (förhandsversion)

Azure Cosmos DB erbjuder nu förhandsversionsstöd för vektorlikhetssökning. Vektorsökning är en grundläggande del av vissa programtyper, inklusive AI, semantisk sökning och andra. Med Azure Cosmos DB kan du lagra vektorer direkt i dina dokument tillsammans med resten av dina data, vilket innebär att du kan utföra alla dina frågor mot en enda databas. Detta kan förenkla arkitekturen avsevärt och ta bort behovet av ytterligare en dedikerad vektordatabaslösning i stacken. Mer information om Azure Cosmos DB-vektorsökning finns i dokumentationen.

När din Azure Cosmos DB-container har konfigurerats korrekt är det enkelt att använda vektorsökning via EF för att lägga till en vektoregenskap och konfigurera den:

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

När det är klart använder du funktionen EF.Functions.VectorDistance() i LINQ-frågor för att utföra vektorlikhetssökning:

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

Mer information finns i dokumentationen för om vektorsökning.

Paginering stöd

Azure Cosmos DB-providern tillåter nu sidnumrering via frågeresultat via fortsättningstoken, vilket är mycket effektivare och kostnadseffektivare än den traditionella användningen av Skip och 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
}

Den nya ToPageAsync-operatorn returnerar en CosmosPage, som exponerar en fortsättningstoken som kan användas för att effektivt återuppta frågan vid ett senare tillfälle och hämta nästa 10 objekt:

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

Mer information finns i dokumentationsavsnittet om sidnumrering.

FromSql för säkrare SQL-frågor

Azure Cosmos DB-providern har tillåtit SQL-frågor via FromSqlRaw. Det API:et kan dock vara känsligt för SQL-inmatningsattacker när användarangivna data interpoleras eller sammanfogas till SQL. I EF 9.0 kan du nu använda den nya metoden FromSql, som alltid integrerar parametriserade data som en parameter utanför SQL:

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

Mer information finns i dokumentationsavsnittet om paginering.

Rollbaserad åtkomst

Azure Cosmos DB för NoSQL innehåller ett inbyggt RBAC-system (rollbaserad åtkomstkontroll). Detta stöds nu av EF9 för alla dataplanoperationer. Dock stöder Azure Cosmos DB SDK inte RBAC för hanteringsplanåtgärder i Azure Cosmos DB. Använd Azure Management API istället för EnsureCreatedAsync med RBAC.

Synkron I/O blockeras nu som standard

Azure Cosmos DB för NoSQL stöder inte synkrona (blockerande) API:er från programkod. Tidigare maskerade EF detta genom att blockera för dig vid asynkrona anrop. Detta uppmuntrar dock både synkron I/O-användning, vilket är dålig praxis, och kan orsaka dödlägen. Från och med EF 9 utlöses därför ett undantag när synkron åtkomst görs. Till exempel:

Synkron I/O kan fortfarande användas för tillfället genom att konfigurera varningsnivån på rätt sätt. Till exempel i OnConfiguring på din DbContext typ:

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

Observera dock att vi planerar att helt ta bort synkroniseringsstöd i EF 11, så börja uppdatera för att använda asynkrona metoder som ToListAsync och SaveChangesAsync så snart som möjligt!

AOT- och förkompilerade frågor

Varning

NativeAOT och frågeförkompilering är mycket experimentella funktioner och är ännu inte lämpade för produktionsanvändning. Det stöd som beskrivs nedan bör ses som infrastruktur mot den slutliga funktionen, som sannolikt kommer att släppas med EF 10. Vi rekommenderar att du experimenterar med det aktuella stödet och rapporterar om dina upplevelser, men rekommenderar att du inte distribuerar EF NativeAOT-program i produktion.

EF 9.0 ger inledande, experimentellt stöd för .NET NativeAOT-, vilket gör det möjligt att publicera kompilerade program i förväg som använder EF för att komma åt databaser. För att stödja LINQ-frågor i NativeAOT-läge förlitar sig EF på frågeförkompilering: den här mekanismen identifierar STATISKA EF LINQ-frågor och genererar C# interceptorer, som innehåller kod för att köra varje specifik fråga. Detta kan avsevärt minska programmets starttid, eftersom det tunga arbetet med bearbetning och kompilering av LINQ-frågor till SQL inte längre sker varje gång programmet startas. I stället innehåller varje frågas interceptor den färdiga SQL-filen för den frågan, samt optimerad kod för att materialisera databasresultat som .NET-objekt.

Till exempel med ett program med följande EF-fråga:

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

EF kommer att generera en C#-interceptor i ditt projekt, som tar över frågekörningen. I stället för att bearbeta frågan och översätta den till SQL varje gång programmet startar, har interceptorn SQL Embedded direkt i den (för SQL Server i det här fallet), vilket gör att programmet kan starta mycket snabbare:

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));

Dessutom innehåller samma interceptor kod för att materialisera .NET-objektet från databasresultat:

var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

Detta använder en annan ny .NET-funktion – osäkra åtkomstmetoder, för att injicera data från databasen i ditt objekts privata fält.

Om du är intresserad av NativeAOT och gillar att experimentera med avancerade funktioner kan du prova det här! Tänk bara på att funktionen bör betraktas som instabil och har för närvarande många begränsningar. Vi förväntar oss att stabilisera den och göra den mer lämplig för produktionsanvändning i EF 10.

Mer information finns på NativeAOT-dokumentationssidan.

LINQ- och SQL-översättning

Precis som med varje version innehåller EF9 ett stort antal förbättringar av LINQ-frågefunktionerna. Nya frågor kan översättas och många SQL-översättningar för scenarier som stöds har förbättrats för både bättre prestanda och läsbarhet.

Antalet förbättringar är för stort för att lista alla här. Nedan markeras några av de viktigaste förbättringarna; se det här numret för en mer komplett lista över det arbete som utförts i 9.0.

Vi vill framhäva Andrea Canciani (@ranma42) för hans många, högkvalitativa bidrag för att optimera SQL som genereras av EF Core!

Komplexa typer: Stöd för GroupBy och ExecuteUpdate

GruppEra

Tips

Koden som visas här kommer från ComplexTypesSample.cs.

EF9 stöder gruppering efter en komplex typinstans. Till exempel:

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

EF översätter detta som gruppering efter varje medlem av den komplexa typen, vilket överensstämmer med semantiken för komplexa typer som värdeobjekt. Till exempel i 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

Tips

Koden som visas här kommer från ExecuteUpdateSample.cs.

På samma sätt har ExecuteUpdate i EF9 också förbättrats för att acceptera komplexa typegenskaper. Varje medlem av den komplexa typen måste dock anges uttryckligen. Till exempel:

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

Detta genererar SQL som uppdaterar varje kolumn som mappats till den komplexa typen:

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'

Tidigare var du tvungen att manuellt lista ut de olika egenskaperna för den komplexa typen i ditt ExecuteUpdate-anrop.

Rensa onödiga element från SQL

Tidigare skapade EF ibland SQL som innehöll element som faktiskt inte behövdes. I de flesta fall behövdes dessa eventuellt i ett tidigare skede av SQL-bearbetningen och lämnades kvar. EF9 beskär nu de flesta sådana element, vilket resulterar i mer kompakt och i vissa fall effektivare SQL.

Tabellrensning

Som ett första exempel innehöll ibland SQL-koden, som genererades av EF, JOIN-anrop till tabeller som inte behövdes i sökfrågan. Tänk på följande modell, som använder TPT-arvsmappning (table-per-type):

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

Om vi sedan utför följande sökfråga för att hämta alla kunder med minst en order:

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

EF8 genererade följande 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])

Observera att frågan innehöll en koppling till tabellen DiscountedOrders även om inga kolumner refererades till den. EF9 genererar en beskärd SQL utan sammanfogning:

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

Projektionsbeskärning

På samma sätt ska vi undersöka följande fråga:

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

På EF8 genererade den här frågan följande SQL:

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

Observera att den [o].[Id] projektionen inte behövs i underfrågan, eftersom det yttre SELECT-uttrycket helt enkelt räknar raderna. EF9 genererar följande i stället:

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

... och projektionen är tom. Detta kanske inte verkar så mycket, men det kan förenkla SQL avsevärt i vissa fall. Du är välkommen att bläddra igenom några av de SQL-ändringarna i testerna för att se effekten.

Översättningar som omfattar STÖRST/MINST

Tips

Koden som visas här kommer från LeastGreatestSample.cs.

Flera nya översättningar har introducerats som använder sql-funktionerna GREATEST och LEAST.

Viktig

Funktionerna GREATEST och LEAST introducerades SQL Server/Azure SQL-databaser i 2022-versionen. Visual Studio 2022 installerar SQL Server 2019 som standard. Vi rekommenderar att du installerar SQL Server Developer Edition 2022 för att prova dessa nya översättningar i EF9.

Till exempel översätts frågor som använder Math.Max eller Math.Min nu för Azure SQL med hjälp av GREATEST respektive LEAST. Till exempel:

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

Den här frågan översätts till följande SQL när ef9 körs mot 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 och Math.Max kan också användas på värdena för en primitiv samling. Till exempel:

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

Den här frågan översätts till följande SQL när ef9 körs mot 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

Slutligen kan RelationalDbFunctionsExtensions.Least och RelationalDbFunctionsExtensions.Greatest användas för att direkt anropa funktionen Least eller Greatest i SQL. Till exempel:

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

Den här frågan översätts till följande SQL när ef9 körs mot 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]

Framtvinga eller förhindra frågeparameterisering

Tips

Koden som visas här kommer från QuerySample.cs.

Förutom i vissa specialfall parameteriserar EF Core variabler som används i en LINQ-fråga, men innehåller konstanter i den genererade SQL-filen. Tänk till exempel på följande frågemetod:

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

Detta översätts till följande SQL och parametrar när du använder 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

Observera att EF skapade en konstant i SQL för ".NET Blog" eftersom det här värdet inte ändras från fråga till fråga. Med hjälp av en konstant kan det här värdet granskas av databasmotorn när du skapar en frågeplan, vilket kan resultera i en effektivare fråga.

Å andra sidan parametriseras värdet för id eftersom samma fråga kan köras med många olika värden för id. Att skapa en konstant i det här fallet skulle leda till förorening av frågecachen med många frågor som bara skiljer sig åt i id värden. Detta är mycket dåligt för databasens övergripande prestanda.

I allmänhet bör dessa standardvärden inte ändras. EF Core 8.0.2 introducerar dock en EF.Constant metod som tvingar EF att använda en konstant även om en parameter skulle användas som standard. Till exempel:

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

Översättningen innehåller nu en konstant för det id värdet:

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

Metoden EF.Parameter

EF9 introducerar EF.Parameter-metoden för att utföra motsatsen. D.v.s. tvinga EF att använda en parameter även om värdet är en konstant i koden. Till exempel:

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

Översättningen innehåller nu en parameter för strängen ".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

Parameteriserade primitiva samlingar

EF8 ändrade hur vissa frågor som använder primitiva samlingar blir översatta. När en LINQ-fråga innehåller en parametriserad primitiv samling konverterar EF innehållet till JSON och skickar det som ett enda parametervärde för frågan:

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

Detta resulterar i följande översättning på 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]
)

På så sätt kan du ha samma SQL-fråga för olika parametriserade samlingar (endast parametervärdet ändras), men i vissa situationer kan det leda till prestandaproblem eftersom databasen inte kan planera frågan optimalt. Metoden EF.Constant kan användas för att återgå till den tidigare översättningen.

Följande fråga använder EF.Constant till detta:

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

Den resulterande SQL:en är följande:

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)

Dessutom introducerar EF9 TranslateParameterizedCollectionsToConstantskontextalternativ som kan användas för att förhindra primitiv samlingsparameterisering för alla frågor. Vi har också lagt till en kompletterande TranslateParameterizedCollectionsToParameters som tvingar parameterisering av primitiva samlingar explicit (detta är standardbeteendet).

Tips

Metoden EF.Parameter åsidosätter kontextalternativet. Om du vill förhindra parameterisering av primitiva samlingar för de flesta av dina frågor (men inte alla) kan du ange kontextalternativet TranslateParameterizedCollectionsToConstants och använda EF.Parameter för de frågor eller enskilda variabler som du vill parametrisera.

Infogade okorrelerade underfrågor

Tips

Koden som visas här kommer från QuerySample.cs.

I EF8 kan en IQueryable som refereras i en annan fråga komma att köras som ett separat databasanrop. Tänk till exempel på följande LINQ-fråga:

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

I EF8 körs frågan för dotnetPosts som en tur och retur och sedan körs de slutliga resultaten som en andra fråga. Till exempel på 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

I EF9 är IQueryable i dotnetPosts inline, vilket resulterar i en enda databasrunda.

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

Aggregera funktioner över underfrågor och aggregeringar på SQL Server

EF9 förbättrar översättningen av vissa komplexa frågor med hjälp av aggregerade funktioner som består av underfrågor eller andra aggregeringsfunktioner. Nedan visas ett exempel på en sådan fråga:

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

Först beräknar SelectLatestPostRating för varje Post som kräver en underfråga vid översättning till SQL. Senare i sökningen aggregeras dessa resultat med hjälp av Average operation. Den resulterande SQL ser ut så här när den körs på 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]

I tidigare versioner skulle EF Core generera ogiltig SQL för liknande frågor och försöka tillämpa aggregeringsåtgärden direkt över underfrågan. Detta är inte tillåtet på SQL Server och resulterar i ett undantag. Samma princip gäller för frågor som använder aggregering över en annan mängd:

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

Not

Den här ändringen påverkar inte Sqlite, som stöder aggregeringar över underfrågor (eller andra aggregat) och inte stöder LATERAL JOIN (APPLY). Nedan visas SQL för den första frågan som körs på 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"

Frågor med antal != 0 är optimerade

Tips

Koden som visas här kommer från QuerySample.cs.

I EF8 översattes följande LINQ-fråga för att använda funktionen SQL COUNT:

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

EF9 genererar nu en effektivare översättning med hjälp av 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#-semantik för jämförelseåtgärder för null-värden

I EF8 utfördes inte jämförelser mellan nullbara element korrekt för vissa scenarier. Om en eller båda operanderna i C# är null är resultatet av en jämförelseåtgärd falskt. I annat fall jämförs de inneslutna värdena för operander. I EF8 använde vi för att översätta jämförelser med hjälp av databasens null-semantik. Detta skulle ge resultat som skiljer sig från liknande fråga med LINQ till objekt. Dessutom skulle vi producera olika resultat när jämförelsen gjordes i filter kontra projektion. Vissa frågor skulle också ge olika resultat mellan Sql Server och Sqlite/Postgres.

Till exempel frågan:

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

skulle generera följande SQL:

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

som filtrerar ut entiteter vars NullableIntOne eller NullableIntTwo är inställda på null.

I EF9 producerar vi:

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)

Liknande jämförelse som utförs i en projektion:

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

resulterade i följande 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]

som returnerar false för entiteter vars NullableIntOne eller NullableIntTwo är inställda på null (i stället för true förväntas i C#). Kör samma scenario på Sqlite som genererats:

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

vilket resulterar i Nullable object must have a value undantag, eftersom översättning ger null värde för fall där NullableIntOne eller NullableIntTwo är null.

EF9 hanterar nu dessa scenarier korrekt och ger resultat som är konsekventa med LINQ till objekt och mellan olika leverantörer.

Den här förbättringen bidrogs av @ranma42. Stort tack!

Översättning av Order- och OrderDescending LINQ-operatorer

EF9 möjliggör översättning av LINQ-förenklade beställningsåtgärder (Order och OrderDescending). Dessa fungerar ungefär som OrderBy/OrderByDescending men kräver inget argument. I stället tillämpar de standardordning – för entiteter innebär det att sortera baserat på primärnyckelvärden och för andra typer, ordning baserat på själva värdena.

Nedan visas en exempelfråga som drar nytta av de förenklade ordningsoperatorerna:

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

Den här frågan motsvarar följande:

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

och skapar följande 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]

Notera

Order och OrderDescending metoder stöds endast för samlingar av entiteter, komplexa typer eller skalärer – de fungerar inte med mer komplexa projektioner, t.ex. samlingar av anonyma typer som innehåller flera egenskaper.

Den här förbättringen bidrogs av EF-teamets alumner @bricelam. Stort tack!

Förbättrad översättning av logisk negationsoperator (!)

EF9 ger många optimeringar kring SQL CASE/WHEN, COALESCE, negation och olika andra konstruktioner; de flesta av dessa bidrogs av Andrea Canciani (@ranma42) - många tack för alla dessa! Nedan beskriver vi bara några av dessa optimeringar kring logisk negation.

Nu ska vi undersöka följande fråga:

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

I EF8 skulle vi skapa följande SQL:

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

I EF9 "pushar" vi NOT-operationen till jämförelsen:

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

Ett annat exempel, som gäller för SQL Server, är en negerad villkorlig åtgärd.

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

I EF8 brukade resultera i kapslade CASE-block:

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]

I EF9-versionen tog vi bort nästningen.

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

När du projicerar en negerad bool-egenskap på SQL Server:

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

EF8 skulle generera ett CASE block eftersom jämförelser inte kan visas i projektionen direkt i SQL Server-frågor:

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]

I EF9 har översättningen förenklats och använder nu bitvis INTE (~):

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

Bättre stöd för Azure SQL och Azure Synapse

EF9 ger mer flexibilitet när du anger vilken typ av SQL Server som är mål. I stället för att konfigurera EF med UseSqlServerkan du nu ange UseAzureSql eller UseAzureSynapse. På så sätt kan EF skapa bättre SQL när du använder Azure SQL eller Azure Synapse. EF kan dra nytta av databasspecifika funktioner (t.ex. dedikerad typ för JSON i Azure SQL) eller kringgå dess begränsningar (t.ex. ESCAPE-satsen inte är tillgänglig när du använder LIKE på Azure Synapse).

Andra frågeförbättringar

  • Stödet för att fråga primitiva samlingar som introducerades i EF8 har utökats för att stödja alla typer av ICollection<T>. Observera att detta endast gäller för parameter- och infogade samlingar – primitiva samlingar som är en del av entiteter är fortfarande begränsade till matriser, listor och i EF9 samt endast läsbara matriser/listor.
  • Nya ToHashSetAsync-funktioner för att returnera resultatet av en fråga som ett HashSet (#30033, bidragit av @wertzui).
  • TimeOnly.FromDateTime och FromTimeSpan översätts nu på SQL Server (#33678).
  • ToString över enum har nu översatts (#33706, bidragit av @Danevandy99).
  • string.Join översätts nu till CONCAT_WS i icke-aggregerad kontext på SQL Server (#28899).
  • EF.Functions.PatIndex översätts nu till funktionen SQL Server PATINDEX, som returnerar startpositionen för den första förekomsten av ett mönster (#33702, @smnsht).
  • Sum och Average fungerar nu för decimaler i SQLite (#33721, som @ranma42).
  • Korrigeringar och optimeringar av string.StartsWith och EndsWith (#31482).
  • Convert.To*-metoder kan nu ta emot argument med typen object (#33891, bidragit av @imangd).
  • Exclusive-Or (XOR) har nu översatts i SQL Server (#34071, bidraget av @ranma42).
  • Optimeringar kring nullbarhet för COLLATE och AT TIME ZONE operationer (#34263, bidraget av @ranma42).
  • Optimeringar för DISTINCT över IN, EXISTS och mängdoperationer (#34381, bidrag av @ranma42).

Ovanstående var bara några av de viktigaste frågeförbättringarna i EF9; se för en mer fullständig lista i den här utgåvan.

Migreringar

Skydd mot samtidiga migreringar

EF9 introducerar en låsningsmekanism för att skydda mot flera migreringskörningar samtidigt, eftersom det kan lämna databasen i ett skadat tillstånd. Detta inträffar inte när migreringar implementeras i produktionsmiljön genom att använda de rekommenderade metoderna , men kan inträffa om migreringar tillämpas under körning genom att använda metoden DbContext.Database.MigrateAsync(). Vi rekommenderar att du tillämpar migreringar vid distribution i stället för som en del av programstarten, men det kan resultera i mer komplicerade programarkitekturer (t.ex. när du använder .NET Aspire-projekt).

Notera

Om du använder Sqlite-databasen kan du läsa potentiella problem som är associerade med den här funktionen.

Varna när flera migreringsåtgärder inte kan köras i en transaktion

De flesta åtgärder som utförs under migreringar skyddas av en transaktion. Detta säkerställer att databasen inte hamnar i ett skadat tillstånd om migreringen av någon anledning misslyckas. Vissa åtgärder är dock inte omslutna i en transaktion (t.ex. åtgärder i SQL Server-minnesoptimerade tabellereller databasförändrande åtgärder som att ändra databassortering). För att undvika att databasen skadas i händelse av migreringsfel rekommenderar vi att dessa åtgärder utförs isolerat med hjälp av en separat migrering. EF9 identifierar nu ett scenario när en migrering innehåller flera åtgärder, varav en inte kan omslutas i en transaktion och utfärdar en varning.

Förbättrad datasåddning

EF9 introducerade ett bekvämt sätt att utföra datasåddning, som fyller databasen med initiala data. DbContextOptionsBuilder innehåller nu UseSeeding och UseAsyncSeeding metoder som körs när DbContext initieras (som en del av EnsureCreatedAsync).

Note

Om programmet hade körts tidigare kanske databasen redan innehåller exempeldata (som skulle ha lagts till vid den första initieringen av kontexten). Därför bör UseSeedingUseAsyncSeeding kontrollera om det finns data innan du försöker fylla i databasen. Detta kan uppnås genom att utfärda en enkel EF-fråga.

Här är ett exempel på hur dessa metoder kan användas:

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

Mer information finns här.

Andra migreringsförbättringar

  • När du ändrar en befintlig tabell till en tidstabell för SQL Server har storleken på migreringskoden minskat avsevärt.

Modellbyggnad

Autokompilerade modeller

Tips

Koden som visas här kommer från exemplet NewInEFCore9.CompiledModels.

Kompilerade modeller kan förbättra starttiden för applikationer med stora modeller – det vill säga antalet entitetstyper i hundra- eller tusental. I tidigare versioner av EF Core måste en kompilerad modell genereras manuellt med hjälp av kommandoraden. Till exempel:

dotnet ef dbcontext optimize

När du har kört kommandot måste en rad som .UseModel(MyCompiledModels.BlogsContextModel.Instance) läggas till i OnConfiguring för att uppmana EF Core att använda den kompilerade modellen.

Från och med EF9 behövs inte längre den här .UseModel raden när programmets DbContext typ finns i samma projekt/sammansättning som den kompilerade modellen. I stället identifieras den kompilerade modellen och används automatiskt. Detta kan ses genom att låta EF logga när det skapar modellen. När du kör ett enkelt program visas hur EF skapar modellen när programmet startar:

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

Utdata från att köra dotnet ef dbcontext optimize i modellprojektet är:

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> 

Observera att loggutdata anger att -modellen skapades när kommandotkördes. Om vi nu kör programmet igen, efter återskapande men utan att göra några kodändringar, är utdata:

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

Observera att modellen inte skapades när programmet startades eftersom den kompilerade modellen identifierades och användes automatiskt.

MSBuild-integrering

Med metoden ovan måste den kompilerade modellen fortfarande återskapas manuellt när entitetstyperna eller DbContext konfigurationen ändras. EF9 levereras dock med ett MSBuild-aktivitetspaket som automatiskt kan uppdatera den kompilerade modellen när modellprojektet skapas! Kom igång genom att installera Microsoft.EntityFrameworkCore.Tasks NuGet-paketet. Till exempel:

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

Tips

Använd paketversionen i kommandot ovan som matchar den version av EF Core som du använder.

Aktivera sedan integreringen genom att ange egenskaperna EFOptimizeContext och EFScaffoldModelStage i .csproj-filen. Till exempel:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>

Om vi nu skapar projektet kan vi se loggning vid byggtiden som anger att den kompilerade modellen byggs:

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 

Och om du kör programmet visas att den kompilerade modellen har identifierats och att modellen därför inte har skapats igen:

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

När modellen ändras återskapas nu den kompilerade modellen automatiskt så snart projektet har skapats.

Mer information finns i MSBuild-integrering.

Skrivskyddade primitiva samlingar

Tips

Koden som visas här kommer från PrimitiveCollectionsSample.cs.

EF8 introducerade stöd för mappningsmatriser och föränderliga listor över primitiva typer. Detta har expanderats i EF9 för att inkludera skrivskyddade samlingar/listor. Mer specifikt stöder EF9 samlingar som skrivs som IReadOnlyList, IReadOnlyCollectioneller ReadOnlyCollection. I följande kod mappas till exempel DaysVisited av konventionen som en primitiv samling datum:

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

Den skrivskyddade samlingen kan stödjas av en vanlig, föränderlig samling om så önskas. I följande kod kan DaysVisited till exempel mappas som en primitiv samling datum, samtidigt som koden i klassen fortfarande kan manipulera den underliggande listan.

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

Dessa samlingar kan sedan användas i frågor på vanligt sätt. Till exempel den här LINQ-frågan:

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

Vilket översätts till följande SQL på 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"

Ange fyllningsfaktor för nycklar och index

Tips

Koden som visas här kommer från ModelBuildingSample.cs.

EF9 stöder specifikation av SQL Server-fyllningsfaktor när du använder EF Core-migreringar för att skapa nycklar och index. "När ett index skapas eller återskapas avgör fyllningsfaktorvärdet procentandelen utrymme på varje sida på lövnivå som ska fyllas med data, vilket reserverar resten på varje sida som ledigt utrymme för framtida tillväxt."

Fyllningsfaktorn kan anges på en enskild eller sammansatt primär och alternativ nyckel och index. Till exempel:

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

När den tillämpas på befintliga tabeller kommer detta att ändra dem enligt fyllningsfaktorn och villkoret.

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

Den här förbättringen bidrogs av @deano-hunter. Stort tack!

Gör befintliga modellbyggkonventioner mer utökningsbara

Tips

Koden som visas här kommer från CustomConventionsSample.cs.

Allmänna byggkonventioner för modeller för applikationer infördes i EF7. I EF9 har vi gjort det lättare att utvidga vissa av de befintliga konventionerna. Till exempel koden för att mappa egenskaper efter attribut i EF7 är följande:

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

I EF9 kan detta förenklas ned till följande:

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

Uppdatera ApplyConfigurationsFromAssembly för att anropa icke-offentliga konstruktorer

I tidigare versioner av EF Core instansierade ApplyConfigurationsFromAssembly-metoden endast konfigurationstyper med offentliga, parameterlösa konstruktorer. I EF9 har vi både förbättrat de felmeddelanden som genererats när detta misslyckasoch även aktiverat instansiering av icke-offentlig konstruktor. Detta är användbart när du samlokaliserar konfigurationen i en privat kapslad klass som aldrig ska instansieras av programkod. Till exempel:

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

Åsido, vissa människor tror att det här mönstret är en styggelse eftersom det kopplar entitetstypen till konfigurationen. Andra tycker att det är mycket användbart eftersom det samplacerar konfigurationen med entitetstypen. Låt oss inte diskutera det här. :-)

SQL Server HierarchyId

Tips

Koden som visas här kommer från HierarchyIdSample.cs.

Socker för HierarchyId-sökvägsgenerering

Förstklassigt stöd för SQL Server HierarchyId typ lades till i EF8. I EF9 har en förenklad metod lagts till för att göra det enklare att skapa nya barnnoder i trädstrukturen. Följande kodfrågor för en befintlig entitet med en HierarchyId-egenskap:

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

Den här HierarchyId egenskapen kan sedan användas för att skapa underordnade noder utan någon explicit strängmanipulering. Till exempel:

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

Om daisy har en HierarchyId av /4/1/3/1/får child1HierarchyId "/4/1/3/1/1/" och child2 får HierarchyId "/4/1/3/1/2/".

Om du vill skapa en nod för dessa två barn kan du använda en extra undernivå. Till exempel:

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

Detta skapar en nod med en HierarchyId av /4/1/3/1/1.5/och placerar den mellan child1 och child2.

Den här förbättringen bidrogs av @Rezakazemi890. Stort tack!

Verktyg

Färre återskapanden

Kommandoradsverktyget dotnet ef som standard skapar projektet innan verktyget körs. Det beror på att det är en vanlig källa till förvirring att inte bygga om innan verktyget körs när saker inte fungerar. Erfarna utvecklare kan använda alternativet --no-build för att undvika den här versionen, vilket kan vara långsamt. Men även alternativet --no-build kan leda till att projektet byggs om nästa gång det byggs utanför EF-verktygen.

Vi tror att ett gemenskapsbidrag från @Suchiman har åtgärdat detta. Men vi är också medvetna om att justeringar kring MSBuild-beteenden har en tendens att få oavsiktliga konsekvenser, så vi ber människor som du att prova detta och rapportera tillbaka om eventuella negativa upplevelser du har.