Condividi tramite


Novità di EF Core 9

EF Core 9 (EF9) è la versione successiva dopo EF Core 8 ed è pianificata per il rilascio a novembre 2024.

EF9 è disponibile come build giornaliere che contengono tutte le funzionalità e le modifiche api più recenti di EF9. Gli esempi qui usano queste build giornaliere.

Suggerimento

È possibile eseguire ed eseguire il debug negli esempi scaricando il codice di esempio da GitHub. Ogni sezione seguente è collegata al codice sorgente specifico di tale sezione.

EF9 è destinato a .NET 8 e può quindi essere usato con .NET 8 (LTS) o con .NET 9 preview.

Suggerimento

La documentazione Novità viene aggiornata per ogni anteprima. Tutti gli esempi sono configurati per l'uso delle build giornaliere di EF9, che in genere hanno diverse settimane di lavoro completate rispetto all'anteprima più recente. È consigliabile usare le build quotidiane durante il test di nuove funzionalità in modo da non eseguire i test su bit non aggiornati.

Azure Cosmos DB for NoSQL

EF 9.0 apporta miglioramenti sostanziali al provider EF Core per Azure Cosmos DB; parti significative del provider sono state riscritte per fornire nuove funzionalità, consentire nuove forme di query e allineare meglio il provider alle procedure consigliate di Azure Cosmos DB. Di seguito sono elencati i principali miglioramenti di alto livello; per un elenco completo, vedere questo problema epico.

Avviso

Come parte dei miglioramenti del provider, è stato necessario apportare una serie di modifiche di rilievo ad alto impatto; se si sta aggiornando un'applicazione esistente, leggere attentamente la sezione modifiche di rilievo.

Miglioramenti dell'esecuzione di query con chiavi di partizione e ID documento

Ogni documento archiviato in un database di Azure Cosmos DB ha un ID risorsa univoco. Inoltre, ogni documento può contenere una "chiave di partizione" che determina il partizionamento logico dei dati in modo che il database possa essere ridimensionato in modo efficace. Maggiori informazioni sulla scelta delle chiavi di partizione sono disponibili in Partizionamento e scalabilità orizzontale in Azure Cosmos DB.

In EF 9.0, il provider Azure Cosmos DB è significativamente migliore per identificare i confronti delle chiavi di partizione nelle query LINQ ed estrarli per assicurarsi che le query vengano inviate solo alla partizione pertinente; ciò può migliorare notevolmente le prestazioni delle query e ridurre gli addebiti per le UR. Ad esempio:

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

In questa query, il provider riconosce automaticamente il confronto in PartitionKey. Se si esaminano i log, verrà visualizzato quanto segue:

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

Si noti che la WHERE clausola non contiene PartitionKey: il confronto è stato "rimosso" e viene usato per eseguire la query solo sulla partizione pertinente. Nelle versioni precedenti, il confronto è rimasto nella WHERE clausola in molte situazioni, causando l'esecuzione della query su tutte le partizioni e con conseguente aumento dei costi e riduzione delle prestazioni.

Inoltre, se la query fornisce anche un valore per la proprietà ID del documento e non include altre operazioni di query, il provider può applicare un'ottimizzazione aggiuntiva:

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

I log mostrano quanto segue per questa query:

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

In questo caso, non viene inviata alcuna query SQL. Il provider esegue invece una lettura di punti (ReadItemAPI), che recupera direttamente il documento in base alla chiave di partizione e all'ID. Questo è il tipo di lettura più efficiente e conveniente che è possibile eseguire in Azure Cosmos DB; per altre informazioni sulle letture dei punti, vedere la documentazione di Azure Cosmos DB.

Per maggiori informazioni sull'esecuzione di query con chiavi di partizione e letture di punti, vedere la pagina della documentazione sull'esecuzione di query.

Chiavi di partizione gerarchiche

Suggerimento

Il codice illustrato di seguito proviene da HierarchicalPartitionKeysSample.cs.

Azure Cosmos DB supportava originariamente una singola chiave di partizione, ma da allora ha ampliato le funzionalità di partizionamento per supportare anche la sottopartizione tramite la specifica di fino a tre livelli di gerarchia nella chiave di partizione. EF Core 9 offre il supporto completo per le chiavi di partizione gerarchica, consentendo di sfruttare le prestazioni migliori e i risparmi sui costi associati a questa funzionalità.

Le chiavi di partizione vengono specificate usando l'API di compilazione del modello, in genere in DbContext.OnModelCreating. Nel tipo di entità deve essere presente una proprietà mappata per ogni livello della chiave di partizione. Si consideri ad esempio un tipo di entità UserSession:

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

Il codice seguente specifica una chiave di partizione a tre livelli usando le proprietà TenantId, UserId e SessionId:

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

Suggerimento

Questa definizione di chiave di partizione segue l'esempio indicato in Scegliere le chiavi di partizione gerarchiche dalla documentazione di Azure Cosmos DB.

Si noti che, a partire da EF Core 9, le proprietà di qualsiasi tipo mappato possono essere usate nella chiave di partizione. Per bool e i tipi numerici, come la proprietà int SessionId, il valore viene usato direttamente nella chiave di partizione. Altri tipi, ad esempio la proprietà Guid UserId, vengono convertiti automaticamente in stringhe.

Durante l'esecuzione di query, Entity Framework estrae automaticamente i valori della chiave di partizione dalle query e li applica all'API di query di Azure Cosmos DB per garantire che le query siano vincolate in modo appropriato al minor numero possibile di partizioni. Si consideri, ad esempio, la query LINQ seguente che fornisce tutti e tre i valori della chiave di partizione nella gerarchia:

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

Quando si esegue questa query, EF Core estrae i valori dei tenantIdparametri , userIde sessionId li passa all'API di query di Azure Cosmos DB come valore della chiave di partizione. Ad esempio, vedere i log dall'esecuzione della query precedente:

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

Si noti che i confronti delle chiavi di partizione sono stati rimossi dalla WHERE clausola e vengono invece usati come chiave di partizione per un'esecuzione efficiente: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

Per maggiori informazioni, consultare la documentazione sull'esecuzione di query con chiavi di partizione.

Funzionalità di query LINQ notevolmente migliorate

In EF 9.0, le funzionalità di traduzione LINQ del provider Azure Cosmos DB sono state notevolmente espanse e il provider può ora eseguire tipi di query significativamente maggiori. L'elenco completo dei miglioramenti delle query è troppo lungo da elencare, ma ecco le principali evidenziazioni:

  • Supporto completo per le raccolte primitive di ENTITY, che consente di eseguire query LINQ su raccolte di stringhe o int. Per maggiori informazioni, consultare la sezione Novità di EF8: raccolte primitive.
  • Supporto per l'esecuzione di query arbitrarie su raccolte non primitive.
  • Sono ora supportati numerosi operatori LINQ aggiuntivi: indicizzazione in raccolte, Length/Count, ElementAtContains, e molte altre.
  • Supporto per gli operatori di aggregazione, ad Count esempio e Sum.
  • Traduzioni di funzioni aggiuntive (vedere la documentazione relativa ai mapping delle funzioni per l'elenco completo delle traduzioni supportate):
    • Traduzioni per DateTime i membri dei componenti e DateTimeOffset (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined e EF.Functions.CoalesceUndefined ora consentono di gestire i undefined valori.
    • string.Contains, StartsWith e EndsWith ora supportano StringComparison.OrdinalIgnoreCase.

Per l'elenco completo dei miglioramenti delle query, vedere questo problema:

Modellazione migliorata allineata agli standard Azure Cosmos DB e JSON

EF 9.0 esegue il mapping ai documenti di Azure Cosmos DB in modi più naturali per un database di documenti basato su JSON e consente di interagire con altri sistemi che accedono ai documenti. Anche se ciò comporta modifiche di rilievo, esistono API che consentono di ripristinare il comportamento pre-9.0 in tutti i casi.

Proprietà semplificate id senza discriminatori

Prima di tutto, le versioni precedenti di EF hanno inserito il valore discriminatorio nella proprietà JSON id, producendo documenti come i seguenti:

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

Questa operazione è stata eseguita per consentire ai documenti di tipi diversi (ad esempio blog e post) e allo stesso valore della chiave (1099) di esistere all'interno della stessa partizione del contenitore. A partire da EF 9.0, la proprietà id contiene solo il valore della chiave:

{
    "id": 1099,
    ...
}

Si tratta di un modo più naturale per eseguire il mapping a JSON e semplifica l'interazione tra strumenti e sistemi esterni con i documenti JSON generati da EF; tali sistemi esterni non sono in genere a conoscenza dei valori discriminatori di EF, che sono per impostazione predefinita derivati dai tipi .NET.

Si noti che si tratta di una modifica che causa un'interruzione, poiché EF non sarà più in grado di eseguire query sui documenti esistenti con il formato precedente id. È stata introdotta un'API per ripristinare il comportamento precedente, vedere la nota di modifica di rilievo e la documentazione per altri dettagli.

Proprietà discriminatoria rinominata in $type

La proprietà discriminatoria predefinita era denominata Discriminatorin precedenza . EF 9.0 modifica l'impostazione predefinita in $type:

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

Questo segue lo standard emergente per il polimorfismo JSON, consentendo una migliore interoperabilità con altri strumenti. Per esempio, System.Text.Json di .NET supporta anche il polimorfismo, usando $type come nome di proprietà discriminatorio predefinito (docs).

Si noti che si tratta di una modifica che causa un'interruzione, poiché EF non sarà più in grado di eseguire query sui documenti esistenti con il nome della proprietà discriminatoria precedente. Per informazioni dettagliate su come ripristinare la denominazione precedente, vedere la nota di modifica che causa un'interruzione.

Ricerca per somiglianza vettoriale (anteprima)

Azure Cosmos DB offre ora il supporto in anteprima per la ricerca di somiglianza vettoriale. La ricerca vettoriale è una parte fondamentale di alcuni tipi di applicazione, tra cui intelligenza artificiale, ricerca semantica e altre. Azure Cosmos DB consente di archiviare i vettori direttamente nei documenti insieme al resto dei dati, ovvero è possibile eseguire tutte le query su un singolo database. Ciò può semplificare notevolmente l'architettura e rimuovere la necessità di una soluzione di database vettoriale dedicata aggiuntiva nello stack. Per altre informazioni sulla ricerca vettoriale di Azure Cosmos DB, vedere la documentazione.

Dopo aver configurato correttamente il contenitore Azure Cosmos DB, l'uso della ricerca vettoriale tramite ENTITY è una semplice questione di aggiunta di una proprietà vettoriale e della relativa configurazione:

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

Al termine, usare la funzione EF.Functions.VectorDistance() nelle query LINQ per eseguire una ricerca di somiglianza vettoriale:

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

Per altre informazioni su questa funzionalità, consultare la documentazione sulla ricerca vettoriale.

Supporto per l'impaginazione

Il provider Azure Cosmos DB consente ora di impaginare i risultati delle query tramite token di continuazione, che è molto più efficiente e conveniente rispetto all'uso tradizionale di Skip e 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
}

Il nuovo ToPageAsync operatore restituisce CosmosPage, che espone un token di continuazione che può essere usato per riprendere in modo efficiente la query in un secondo momento, recuperando i 10 elementi successivi:

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

Per maggiori informazioni, consultare la sezione della documentazione sulla paginazione.

FromSql per query SQL più sicure

Il provider Azure Cosmos DB ha consentito l'esecuzione di query SQL tramite FromSqlRaw. Tuttavia, tale API può essere soggetta ad attacchi SQL injection quando i dati forniti dall'utente vengono interpolati o concatenati in SQL. In EF 9.0 è ora possibile usare il nuovo metodo FromSql, che integra sempre i dati con parametri come parametro all'esterno di SQL:

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

Per maggiori informazioni, consultare la sezione della documentazione sulla paginazione.

Accesso in base al ruolo

Azure Cosmos DB per NoSQL include un sistema predefinito di controllo degli accessi in base al ruolo. Questa funzionalità è ora supportata da EF9 per tutte le operazioni del piano dati. Azure Cosmos DB SDK non supporta tuttavia il controllo degli accessi in base al ruolo per le operazioni del piano di gestione in Azure Cosmos DB. Usare l'API di gestione di Azure anziché con il controllo degli accessi in base al EnsureCreatedAsync ruolo.

L'I/O sincrono è ora bloccato per impostazione predefinita

Azure Cosmos DB per NoSQL non supporta LE API sincrone (bloccanti) dal codice dell'applicazione. In precedenza, EF mascherava questo bloccando le chiamate asincrone. Tuttavia, entrambi incoraggiano l'uso di I/O sincrono, che è una procedura non valida e possono causare deadlock. Pertanto, a partire da EF 9, viene generata un'eccezione quando si tenta l'accesso sincrono. Ad esempio:

Le operazioni di I/O sincrone possono essere ancora usate per il momento configurando il livello di avviso in modo appropriato. Ad esempio, in OnConfiguring nel tipo DbContext:

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

Si noti, tuttavia, che si prevede di rimuovere completamente il supporto per la sincronizzazione in EF 11, quindi iniziare l'aggiornamento per usare metodi asincroni come ToListAsync e SaveChangesAsync il prima possibile.

AOT e query precompilata

Come accennato nell'introduzione, è in corso un sacco di lavoro dietro le quinte per consentire l'esecuzione di EF Core senza compilazione JIT (Just-In-Time). Ef compila invece tutti gli elementi necessari per eseguire query nell'applicazione. Questa compilazione AOT e l'elaborazione correlata verranno eseguite durante la compilazione e la pubblicazione dell'applicazione. A questo punto della versione di EF9, non è disponibile molto che può essere usato dall'utente, lo sviluppatore di app. Tuttavia, per gli interessati, i problemi completati in EF9 che supportano query AOT e precompilata sono:

Vedere qui per alcuni esempi di come usare query precompilata man mano che l'esperienza si riunisce.

Traduzione LINQ e SQL

Analogamente a ogni versione, EF9 include numerosi miglioramenti alle funzionalità di query LINQ. È possibile tradurre nuove query e molte traduzioni SQL per gli scenari supportati sono state migliorate per migliorare le prestazioni e la leggibilità.

Il numero di miglioramenti è troppo grande per elencarli tutti qui. Di seguito sono evidenziati alcuni dei miglioramenti più importanti; vedere questo problema per un elenco più completo del lavoro svolto nella versione 9.0.

Vorremmo chiamare Andrea Canciani (@ranma42) per i suoi numerosi contributi di alta qualità all'ottimizzazione di SQL generato da EF Core.

Tipi complessi: supporto di GroupBy e ExecuteUpdate

GroupBy

Suggerimento

Il codice illustrato di seguito proviene da ComplexTypesSample.cs.

EF9 supporta il raggruppamento in base a un'istanza di tipo complesso. Ad esempio:

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

EF lo converte come raggruppamento in base a ogni membro del tipo complesso, allineato alla semantica dei tipi complessi come oggetti valore. Ad esempio, 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

Suggerimento

Il codice illustrato di seguito proviene da ExecuteUpdateSample.cs.

Analogamente, in EF9 ExecuteUpdate è stato migliorato anche per accettare proprietà di tipo complesse. Tuttavia, ogni membro del tipo complesso deve essere specificato in modo esplicito. Ad esempio:

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

Viene generato l'errore SQL che aggiorna ogni colonna mappata al tipo complesso:

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'

In precedenza, era necessario elencare manualmente le diverse proprietà del tipo complesso nella chiamata ExecuteUpdate.

Eliminare gli elementi non contenuti da SQL

In precedenza, EF ha talvolta prodotto SQL che conteneva elementi che non erano effettivamente necessari; nella maggior parte dei casi, questi erano probabilmente necessari in una fase precedente dell'elaborazione SQL e sono stati lasciati indietro. EF9 elimina ora la maggior parte di tali elementi, con conseguente maggiore compatta e, in alcuni casi, sql più efficiente.

Eliminazione tabella

Come primo esempio, SQL generato da EF a volte conteneva JOIN alle tabelle che non erano effettivamente necessarie nella query. Si consideri il modello seguente, che usa il mapping di ereditarietà della tabella per tipo (TPT):

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

    public Customer Customer { get; set; }
}

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

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

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

public class BlogContext : DbContext
{
    ...

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

Se si esegue quindi la query seguente per ottenere tutti i clienti con almeno un ordine:

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

EF8 ha generato il seguente 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])

Si noti che la query contiene un join alla tabella DiscountedOrders anche se non vi è stato fatto riferimento a colonne. EF9 genera un'istanza di SQL eliminati senza il join:

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

Eliminazione della proiezione

Analogamente, esaminiamo la query seguente:

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

In EF8, questa query ha generato il codice SQL seguente:

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

Si noti che la [o].[Id] proiezione non è necessaria nella sottoquery, poiché l'espressione SELECT esterna conta semplicemente le righe. EF9 genera invece quanto segue:

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

... e la proiezione è vuota. Questo potrebbe non sembrare molto, ma può semplificare significativamente sql in alcuni casi; è possibile scorrere alcune delle modifiche DI SQL nei test per visualizzare l'effetto.

Traduzioni che coinvolgono GREATEST/LEAST

Suggerimento

Il codice illustrato di seguito proviene da LeastGreatestSample.cs.

Sono state introdotte diverse nuove traduzioni che usano GREATEST e le funzioni SQL LEAST.

Importante

Le GREATEST funzioni e LEAST sono state introdotte nei database SQL Server/Sql di Azure nella versione 2022. Visual Studio 2022 installa SQL Server 2019 per impostazione predefinita. È consigliabile installare SQL Server Developer Edition 2022 per provare queste nuove traduzioni in EF9.

Ad esempio, le query che usano Math.Max o Math.Min sono ora convertite per SQL di Azure usando GREATEST e LEAST rispettivamente. Ad esempio:

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

Questa query viene convertita nel codice SQL seguente quando si usa EF9 in esecuzione su 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 e Math.Max possono essere usati anche sui valori di una raccolta primitiva. Ad esempio:

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

Questa query viene convertita nel codice SQL seguente quando si usa EF9 in esecuzione su 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

Infine, RelationalDbFunctionsExtensions.Least e RelationalDbFunctionsExtensions.Greatest può essere usato per richiamare direttamente la Least funzione o Greatest in SQL. Ad esempio:

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

Questa query viene convertita nel codice SQL seguente quando si usa EF9 in esecuzione su 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]

Forzare o impedire la parametrizzazione delle query

Suggerimento

Il codice illustrato di seguito proviene da QuerySample.cs.

Ad eccezione di alcuni casi speciali, EF Core parametrizza le variabili usate in una query LINQ, ma include costanti in SQL generato. Si consideri ad esempio il metodo query seguente:

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

Ciò si traduce nei parametri e SQL seguenti quando si usa 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

Si noti che EF ha creato una costante in SQL per ".NET Blog" perché questo valore non cambierà dalla query alla query. L'uso di una costante consente di esaminare questo valore dal motore di database durante la creazione di un piano di query, generando potenzialmente una query più efficiente.

D'altra parte, il valore di id è parametrizzato, poiché la stessa query può essere eseguita con molti valori diversi per id. La creazione di una costante in questo caso comporta l'inquinamento della cache delle query con molte query che differiscono solo in valori id. Si tratta di un problema molto negativo per le prestazioni complessive del database.

In generale, queste impostazioni predefinite non devono essere modificate. Ef Core 8.0.2 introduce tuttavia un metodo EF.Constant che impone a EF di usare una costante anche se per impostazione predefinita viene usato un parametro. Ad esempio:

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

La traduzione contiene ora una costante per il valore id:

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

Metodo EF.Parameter

EF9 introduce il metodo EF.Parameter per eseguire l'operazione opposta. Vale a dire, forzare EF a usare un parametro anche se il valore è una costante nel codice. Ad esempio:

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

La traduzione contiene ora un parametro per la stringa ".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

Raccolte primitive con parametri

EF8 ha modificato il modo in cui vengono convertite alcune query che usano raccolte primitive. Quando una query LINQ contiene una raccolta primitiva con parametri, EF converte il contenuto in JSON e lo passa come singolo valore del parametro della query:

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

Ciò comporterà la traduzione seguente 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]
)

Ciò consente di avere la stessa query SQL per raccolte con parametri diversi (solo il valore del parametro cambia), ma in alcune situazioni può causare problemi di prestazioni perché il database non è in grado di pianificare in modo ottimale per la query. Il EF.Constant metodo può essere utilizzato per ripristinare la traduzione precedente.

A tale effetto viene EF.Constant usata la query seguente:

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

Il codice SQL risultante è il seguente:

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)

EF9 introduce TranslateParameterizedCollectionsToConstants inoltre l'opzione di contesto che può essere usata per impedire la parametrizzazione della raccolta primitiva per tutte le query. È stato anche aggiunto un complemento TranslateParameterizedCollectionsToParameters che forza la parametrizzazione delle raccolte primitive in modo esplicito (si tratta del comportamento predefinito).

Suggerimento

Il EF.Parameter metodo esegue l'override dell'opzione di contesto. Se si desidera impedire la parametrizzazione delle raccolte primitive per la maggior parte delle query (ma non tutte), è possibile impostare l'opzione TranslateParameterizedCollectionsToConstants di contesto e usare EF.Parameter per le query o le singole variabili da parametrizzare.

Sottoquery non correlate inline

Suggerimento

Il codice illustrato di seguito proviene da QuerySample.cs.

In EF8, un oggetto IQueryable a cui viene fatto riferimento in un'altra query può essere eseguito come round trip separato del database. Si consideri, ad esempio, la query LINQ seguente:

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

In EF8 la query per dotnetPosts viene eseguita come round trip e quindi i risultati finali vengono eseguiti come seconda query. Ad esempio, in 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, IQueryable dotnetPosts in è inlined, con conseguente round trip di un database singolo:

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

Funzioni di aggregazione su sottoquery e aggregazioni in SQL Server

EF9 migliora la conversione di alcune query complesse usando funzioni di aggregazione composte su sottoquery o altre funzioni di aggregazione. Di seguito è riportato un esempio di query di questo tipo:

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

In primo luogo, Select calcola LatestPostRating per ogni Post oggetto che richiede una sottoquery durante la conversione in SQL. Più avanti nella query questi risultati vengono aggregati tramite Average l'operazione . Il codice SQL risultante è simile al seguente quando viene eseguito in 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]

Nelle versioni precedenti EF Core genera sql non valido per query simili, provando ad applicare l'operazione di aggregazione direttamente sulla sottoquery. Questo non è consentito in SQL Server e genera un'eccezione. Lo stesso principio si applica alle query che usano l'aggregazione su un'altra aggregazione:

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

Nota

Questa modifica non influisce su Sqlite, che supporta aggregazioni su sottoquery (o altre aggregazioni) e non supporta LATERAL JOIN (APPLY). Di seguito è riportato il codice SQL per la prima query in esecuzione in 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"

Le query che usano Count != 0 sono ottimizzate

Suggerimento

Il codice illustrato di seguito proviene da QuerySample.cs.

In EF8, la query LINQ seguente è stata convertita per usare la funzione SQL COUNT:

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

EF9 genera ora una traduzione più efficiente usando 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")

Semantica C# per operazioni di confronto su valori nullable

Nei confronti di EF8 tra elementi nullable non sono stati eseguiti correttamente per alcuni scenari. In C#, se uno o entrambi gli operandi sono null, il risultato di un'operazione di confronto è false. In caso contrario, vengono confrontati i valori contenuti degli operandi. In EF8 è stato usato per tradurre i confronti usando la semantica null del database. In questo modo si ottengono risultati diversi rispetto a una query simile usando LINQ to Objects. Inoltre, si produrrebbero risultati diversi quando il confronto è stato eseguito nel filtro e nella proiezione. Alcune query generano anche risultati diversi tra Sql Server e Sqlite/Postgres.

Ad esempio, la query:

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

genera il codice SQL seguente:

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

che filtra le entità le cui entità NullableIntOne o NullableIntTwo sono impostate su Null.

In EF9 viene prodotto:

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)

Confronto simile eseguito in una proiezione:

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

risultante nel seguente 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]

che restituisce false per le entità i cui NullableIntOne valori o NullableIntTwo sono impostati su Null (anziché true quelli previsti in C#). Esecuzione dello stesso scenario in Sqlite generato:

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

che genera un'eccezione Nullable object must have a value , poiché la traduzione produce null valore per i casi in cui NullableIntOne o NullableIntTwo sono Null.

EF9 ora gestisce correttamente questi scenari, producendo risultati coerenti con LINQ to Objects e tra provider diversi.

Questo miglioramento è stato contribuito da @ranma42. Grazie mille!

Conversione di Order operatori e OrderDescending LINQ

EF9 consente la traduzione delle operazioni di ordinamento semplificate LINQ (Order e OrderDescending). Queste operazioni sono simili a OrderBy/OrderByDescending ma non richiedono un argomento. Applicano invece l'ordinamento predefinito: per le entità questo significa ordinare in base ai valori della chiave primaria e ad altri tipi, ordinando in base ai valori stessi.

Di seguito è riportata una query di esempio che sfrutta gli operatori di ordinamento semplificati:

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

La query equivale a quanto segue:

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

e produce il codice SQL seguente:

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]

Nota

Order I metodi e OrderDescending sono supportati solo per raccolte di entità, tipi complessi o scalari. Non funzioneranno in proiezioni più complesse, ad esempio raccolte di tipi anonimi contenenti più proprietà.

Questo miglioramento è stato contribuito dall'alumnus ef team @bricelam. Grazie mille!

Miglioramento della traduzione dell'operatore di negazione logica (!)

EF9 offre molte ottimizzazioni relative a SQL CASE/WHEN, COALESCEnegazione e vari altri costrutti. Molti di questi sono stati forniti da Andrea Canciani (@ranma42) - molti grazie per tutti questi. Di seguito verranno illustrati in dettaglio solo alcune di queste ottimizzazioni relative alla negazione logica.

Esaminiamo la query seguente:

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

In EF8 viene prodotto il codice SQL seguente:

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

In EF9 viene eseguita l'operazione "push" NOT nel confronto:

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

Un altro esempio, applicabile a SQL Server, è un'operazione condizionale negata.

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

In EF8 usato per generare blocchi annidati CASE:

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 è stato rimosso l'annidamento:

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

In SQL Server, quando si proietta una proprietà bool negata:

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

EF8 genera un blocco CASE perché i confronti non possono essere visualizzati nella proiezione direttamente nelle query di SQL Server:

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

In EF9 questa traduzione è stata semplificata e ora usa NOT bit per bit (~):

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

Supporto migliore per Azure SQL e Azure Synapse

EF9 consente una maggiore flessibilità quando si specifica il tipo di SQL Server di destinazione. Anziché configurare Entity Framework con UseSqlServer, è ora possibile specificare UseAzureSql o UseAzureSynapse. Ciò consente a ENTITY Framework di produrre SQL migliori quando si usa Azure SQL o Azure Synapse. Entity Framework può sfruttare le funzionalità specifiche del database (ad esempio un tipo dedicato per JSON in Azure SQL) o aggirare le limitazioni( adESCAPE esempio, la clausola non è disponibile quando si usa LIKE in Azure Synapse).

Altri miglioramenti delle query

  • Il supporto delle query sulle raccolte primitive introdotte in EF8 è stato esteso per supportare tutti i ICollection<T> tipi. Si noti che questo vale solo per le raccolte di parametri e inline: le raccolte primitive che fanno parte delle entità sono ancora limitate a matrici, elenchi e in EF9 matrici/elenchi di sola lettura.
  • Nuove ToHashSetAsync funzioni per restituire i risultati di una query come oggetto HashSet (#30033, fornito da @wertzui).
  • TimeOnly.FromDateTime e FromTimeSpan sono ora tradotti in SQL Server (#33678).
  • ToString over enumerazioni è ora tradotto (#33706, fornito da @Danevandy99).
  • string.Join ora viene convertito in CONCAT_WS nel contesto non aggregato in SQL Server (#28899).
  • EF.Functions.PatIndex ora viene convertito nella funzione SQL Server PATINDEX , che restituisce la posizione iniziale della prima occorrenza di un criterio (#33702, @smnsht).
  • Sum e Average ora funzionano per i decimali in SQLite (#33721, fornito da @ranma42).
  • Correzioni e ottimizzazioni in string.StartsWith e EndsWith (#31482).
  • Convert.To*i metodi possono ora accettare l'argomento di tipo object (#33891, fornito da @imangd).
  • L'operazione Exclusive-Or (XOR) viene ora convertita in SQL Server (#34071, fornita da @ranma42).
  • Ottimizzazioni relative al supporto dei valori Null per COLLATE le operazioni e AT TIME ZONE (#34263, contributo di @ranma42).
  • Ottimizzazioni per DISTINCT le operazioni su INEXISTS e set (#34381, fornite da @ranma42).

I precedenti sono stati solo alcuni dei miglioramenti più importanti delle query in EF9; vedere questo problema per un elenco più completo.

Migrazioni

Migrazioni di tabelle temporali migliorate

La migrazione creata quando si modifica una tabella esistente in una tabella temporale è stata ridotta di dimensioni per EF9. Ad esempio, in EF8 la creazione di una singola tabella esistente comporta la migrazione seguente:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

In EF9, la stessa operazione comporta ora una migrazione molto più piccola:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

Protezione dalle migrazioni simultanee

EF9 introduce un meccanismo di blocco per proteggere da più esecuzioni di migrazione eseguite contemporaneamente, in quanto ciò potrebbe lasciare il database in uno stato danneggiato. Ciò non si verifica quando le migrazioni vengono distribuite nell'ambiente di produzione usando metodi consigliati, ma possono verificarsi se le migrazioni vengono applicate in fase di esecuzione usando il DbContext.Database.Migrate() metodo . È consigliabile applicare migrazioni alla distribuzione, anziché come parte dell'avvio dell'applicazione, ma ciò può comportare architetture di applicazioni più complesse, ad esempio quando si usano progetti .NET Aspire.

Nota

Se si usa il database Sqlite, vedere potenziali problemi associati a questa funzionalità.

Avvisa quando non è possibile eseguire più operazioni di migrazione all'interno di una transazione

La maggior parte delle operazioni eseguite durante le migrazioni è protetta da una transazione. In questo modo, se per qualche motivo la migrazione non riesce, il database non si verifica in uno stato danneggiato. Tuttavia, alcune operazioni non vengono incluse in una transazione, ad esempio operazioni su tabelle ottimizzate per la memoria di SQL Server o operazioni di modifica del database, ad esempio la modifica delle regole di confronto del database. Per evitare di danneggiare il database in caso di errore di migrazione, è consigliabile eseguire queste operazioni in isolamento usando una migrazione separata. EF9 ora rileva uno scenario quando una migrazione contiene più operazioni, una delle quali non può essere sottoposta a wrapping in una transazione e genera un avviso.

Seeding dei dati migliorato

EF9 ha introdotto un modo pratico per eseguire il seeding dei dati, che popola il database con i dati iniziali. DbContextOptionsBuilder ora contiene UseSeeding i metodi e UseAsyncSeeding che vengono eseguiti quando DbContext viene inizializzato (come parte di EnsureCreatedAsync).

Nota

Se l'applicazione fosse stata eseguita in precedenza, il database potrebbe contenere già i dati di esempio, che sarebbero stati aggiunti alla prima inizializzazione del contesto. Di conseguenza, UseSeeding UseAsyncSeeding verificare se i dati esistono prima di tentare di popolare il database. A tale scopo, è possibile eseguire una semplice query EF.

Di seguito è riportato un esempio di come usare questi metodi:

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

Altre informazioni sono disponibili qui.

Creazione del modello

Modelli compilati automaticamente

Suggerimento

Il codice illustrato di seguito deriva dall'esempio NewInEFCore9.CompiledModels .

I modelli compilati possono migliorare il tempo di avvio per le applicazioni con modelli di grandi dimensioni, ovvero i conteggi dei tipi di entità negli anni 100 o 1000. Nelle versioni precedenti di EF Core era necessario generare manualmente un modello compilato, usando la riga di comando. Ad esempio:

dotnet ef dbcontext optimize

Dopo aver eseguito il comando, è necessario aggiungere una riga simile .UseModel(MyCompiledModels.BlogsContextModel.Instance) a per indicare a OnConfiguring EF Core di usare il modello compilato.

A partire da EF9, questa .UseModel riga non è più necessaria quando il tipo dell'applicazione DbContext si trova nello stesso progetto/assembly del modello compilato. Il modello compilato verrà invece rilevato e usato automaticamente. Questo problema può essere visualizzato con il log di Entity Framework ogni volta che compila il modello. L'esecuzione di un'applicazione semplice mostra quindi EF che compila il modello all'avvio dell'applicazione:

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

L'output dell'esecuzione dotnet ef dbcontext optimize nel progetto di modello è:

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> 

Si noti che l'output del log indica che il modello è stato compilato durante l'esecuzione del comando . Se ora si esegue nuovamente l'applicazione, dopo la ricompilazione ma senza apportare modifiche al codice, l'output è:

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

Si noti che il modello non è stato compilato all'avvio dell'applicazione perché il modello compilato è stato rilevato e usato automaticamente.

Integrazione di MSBuild

Con l'approccio precedente, il modello compilato deve comunque essere rigenerato manualmente quando vengono modificati i tipi di entità o DbContext la configurazione. EF9 viene tuttavia fornito con il pacchetto MSBuild e targets che può aggiornare automaticamente il modello compilato quando viene compilato il progetto di modello. Per iniziare, installare il pacchetto NuGet Microsoft.EntityFrameworkCore.Tasks . Ad esempio:

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

Suggerimento

Usare la versione del pacchetto nel comando precedente che corrisponde alla versione di EF Core in uso.

Abilitare quindi l'integrazione impostando la proprietà EFOptimizeContext sul file .csproj. Ad esempio:

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

Sono disponibili proprietà aggiuntive, facoltative, MSBuild per controllare la modalità di compilazione del modello, equivalenti alle opzioni passate dalla riga di comando a dotnet ef dbcontext optimize. tra cui:

Proprietà MSBuild Descrizione
EFOptimizeContext Impostare su true per abilitare i modelli compilati automaticamente.
DbContextName Classe DbContext da utilizzare. Nome della classe solo o completo con spazi dei nomi. Se questa opzione viene omessa, EF Core troverà la classe di contesto. Se sono presenti più classi di contesto, questa opzione è obbligatoria.
EFStartupProject Percorso relativo del progetto di avvio. Il valore predefinito è la cartella corrente.
EFTargetNamespace Spazio dei nomi da usare per tutte le classi generate. Il valore predefinito è generato dallo spazio dei nomi radice e dalla directory di output più CompiledModels.

Nell'esempio è necessario specificare il progetto di avvio:

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

Ora, se si compila il progetto, è possibile visualizzare la registrazione in fase di compilazione che indica che il modello compilato è in fase di compilazione:

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.dll 
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

L'esecuzione dell'applicazione mostra che il modello compilato è stato rilevato e quindi il modello non viene compilato di nuovo:

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

A questo punto, ogni volta che il modello viene modificato, il modello compilato verrà ricompilato automaticamente non appena viene compilato il progetto.

Nota

Vengono illustrati alcuni problemi di prestazioni con le modifiche apportate al modello compilato in EF8 e EF9. Per maggiori informazioni, vedere il problema N. 33483.

Raccolte primitive di sola lettura

Suggerimento

Il codice illustrato di seguito proviene da PrimitiveCollectionsSample.cs.

EF8 ha introdotto il supporto per matrici di mapping e elenchi modificabili di tipi primitivi. Questa operazione è stata espansa in EF9 per includere raccolte/elenchi di sola lettura. In particolare, EF9 supporta raccolte tipizzate come IReadOnlyList, IReadOnlyCollectiono ReadOnlyCollection. Nel codice seguente, ad esempio, DaysVisited verrà eseguito il mapping per convenzione come raccolta primitiva di date:

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

La raccolta di sola lettura può essere supportata da una raccolta normale modificabile, se necessario. Ad esempio, nel codice seguente, DaysVisited può essere mappato come una raccolta primitiva di date, consentendo al codice nella classe di modificare l'elenco sottostante.

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

Queste raccolte possono quindi essere usate nelle query in modo normale. Ad esempio, la query LINQ seguente:

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

Che si traduce nel codice SQL seguente in 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"

Specificare il fattore di riempimento per chiavi e indici

Suggerimento

Il codice illustrato di seguito proviene da ModelBuildingSample.cs.

EF9 supporta la specifica del fattore di riempimento di SQL Server quando si usano le migrazioni di EF Core per creare chiavi e indici. Dalla documentazione su SQL Server, "Quando si crea o si ricompila un indice, il valore del fattore di riempimento determina la percentuale di spazio in ogni pagina al livello foglia da riempire di dati, riservando lo spazio rimanente in ogni pagina come spazio libero per la crescita futura."

Il fattore di riempimento può essere impostato su una singola o composita chiave primaria e alternativa e indici. Ad esempio:

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

Se applicato alle tabelle esistenti, le tabelle verranno modificate con il fattore di riempimento al vincolo :

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

Questo miglioramento è stato contribuito da @deano cacciatore. Grazie mille!

Rendere più estendibili le convenzioni di compilazione di modelli esistenti

Suggerimento

Il codice illustrato di seguito proviene da CustomConventionsSample.cs.

Le convenzioni di compilazione di modelli pubblici per le applicazioni sono state introdotte in EF7. In EF9 è stato reso più semplice estendere alcune delle convenzioni esistenti. Ad esempio, il codice per eseguire il mapping delle proprietà in base all'attributo in EF7 è il seguente:

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 è possibile semplificare le operazioni seguenti:

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

Aggiornare ApplyConfigurationsFromAssembly per chiamare costruttori non pubblici

Nelle versioni precedenti di EF Core, il metodo crea solo un'istanza ApplyConfigurationsFromAssembly dei tipi di configurazione con costruttori pubblici senza parametri. In EF9 sono stati migliorati i messaggi di errore generati quando l'operazione ha esito negativo ed è stata abilitata anche l'istanza da parte di un costruttore non pubblico. Ciò è utile quando si individua la configurazione condivisa in una classe annidata privata che non deve mai essere creata un'istanza dal codice dell'applicazione. Ad esempio:

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

Da parte, alcune persone pensano che questo modello sia un abominio perché associa il tipo di entità alla configurazione. Altre persone pensano che sia molto utile perché individua la configurazione con il tipo di entità. Non parliamo di questo qui. :-)

SQL Server HierarchyId

Suggerimento

Il codice illustrato di seguito proviene da HierarchyIdSample.cs.

Sugar per la generazione del percorso HierarchyId

Il supporto di prima classe per il tipo di SQL Server HierarchyId è stato aggiunto in EF8. In EF9 è stato aggiunto un metodo sugar per semplificare la creazione di nuovi nodi figlio nella struttura ad albero. Ad esempio, il codice seguente esegue una query per un'entità esistente con una proprietà HierarchyId:

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

Questa proprietà HierarchyId può quindi essere usata per creare nodi figlio senza alcuna modifica esplicita delle stringhe. Ad esempio:

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

Se daisy ha un HierarchyId valore di /4/1/3/1/, child1 otterrà " HierarchyId /4/1/1/3/1/1/" e child2 otterrà " HierarchyId /4/1/3/1/2/".

Per creare un nodo tra questi due elementi figlio, è possibile usare un sottolivello aggiuntivo. Ad esempio:

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

In questo modo viene creato un nodo con un HierarchyId di /4/1/3/1/1.5/, inserendolo tra child1 e child2.

Questo miglioramento è stato contribuito da @Rezakazemi890. Grazie mille!

Strumenti

Meno ricompilazione

Lo dotnet ef strumento da riga di comando, per impostazione predefinita, compila il progetto prima di eseguire lo strumento. Ciò è dovuto al fatto che la ricompilazione prima di eseguire lo strumento è una fonte comune di confusione quando le cose non funzionano. Gli sviluppatori esperti possono usare l'opzione --no-build per evitare questa compilazione, che potrebbe essere lenta. Tuttavia, anche l'opzione --no-build potrebbe causare la ri-compilazione del progetto alla successiva compilazione all'esterno degli strumenti di Entity Framework.

Crediamo che un contributo della comunità di @Suchiman abbia risolto questo problema. Tuttavia, siamo consapevoli anche che le modifiche apportate ai comportamenti di MSBuild hanno una tendenza ad avere conseguenze impreviste, quindi chiediamo a persone come te di provare questo e segnalare eventuali esperienze negative che hai.