Novità di EF Core 9

EF Core 9 (EF9) è la versione successiva dopo EF Core 8 ed è pianificata per il rilascio a novembre 2024. Per informazioni dettagliate, vedere Pianificare Entity Framework Core 9 .

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 un'anteprima di .NET 9.

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

Microsoft sta lavorando a aggiornamenti significativi in EF9 al provider di database EF Core per Azure Cosmos DB per NoSQL.

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 la gestione e l'uso di contenitori. Non sono necessarie modifiche al codice dell'applicazione. Per altre informazioni, vedere Problema 32197 .

Accesso sincrono bloccato per impostazione predefinita

Suggerimento

Il codice illustrato di seguito proviene da CosmosSyncApisSample.cs.

Azure Cosmos DB per NoSQL non supporta l'accesso sincrono (blocco) dal codice dell'applicazione. In precedenza, EF mascherava questa impostazione per impostazione predefinita bloccando automaticamente le chiamate asincrone. Tuttavia, entrambi incoraggiano l'uso della sincronizzazione, che è una procedura non valida e possono causare deadlock. Pertanto, a partire da EF9, viene generata un'eccezione quando si tenta l'accesso sincrono. Ad esempio:

System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.SyncNotSupported':
 Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using
 Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
 This exception can be suppressed or logged by passing event ID 'CosmosEventId.SyncNotSupported' to the 'ConfigureWarnings'
 method in 'DbContext.OnConfiguring' or 'AddDbContext'.
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal.CosmosLoggerExtensions.SyncNotSupported(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosClientWrapper.DeleteDatabase()
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosDatabaseCreator.EnsureDeleted()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()

Come indicato dall'eccezione, l'accesso alla sincronizzazione può ancora essere usato per il momento configurando il livello di avviso in modo appropriato. Ad esempio, in nel OnConfiguring 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 EF11, quindi iniziare l'aggiornamento per usare metodi asincroni come ToListAsync e SaveChangesAsync il prima possibile.

Raccolte primitive avanzate

Suggerimento

Il codice illustrato di seguito proviene da CosmosPrimitiveTypesSample.cs.

Il provider Cosmos DB supporta raccolte primitive in forma limitata a partire da EF Core 6. Questo supporto è in fase di miglioramento in EF9, a partire dal consolidamento delle superfici di metadati e API per le raccolte primitive nei database di documenti per allinearsi alle raccolte primitive nei database relazionali. Ciò significa che le raccolte primitive possono ora essere mappate in modo esplicito usando l'API di compilazione del modello, consentendo la configurazione di facet del tipo di elemento. Ad esempio, per eseguire il mapping di un elenco di stringhe obbligatorie (ad esempio non Null):

modelBuilder.Entity<Book>()
    .PrimitiveCollection(e => e.Quotes)
    .ElementType(b => b.IsRequired());

Per altre informazioni sull'API di compilazione del modello, vedere Novità di EF8: raccolte primitive.

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

Il team sta lavorando ad alcune modifiche significative dell'architettura alla pipeline di query in EF Core 9 come parte dei miglioramenti continui apportati ai database json e ai database di documenti. Questo significa che è necessario far sì che gli utenti come te eseguano il codice in questi nuovi elementi interni. (Se stai leggendo un documento "Novità" a questo punto della versione, allora sei una parte davvero impegnata della community; grazie!) Abbiamo più di 120.000 test, ma non è sufficiente! Abbiamo bisogno di te, le persone che eseguono codice reale sui nostri bit, per trovare problemi e spedire una versione solida!

Tipi complessi 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]

Eliminare le colonne passate alla clausola WITH di OPENJSON

Suggerimento

Il codice illustrato di seguito proviene da JsonColumnsSample.cs.

EF9 rimuove le colonne non necessarie quando si chiama OPENJSON WITH. Si consideri, ad esempio, una query che ottiene un conteggio da una raccolta JSON usando un predicato:

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

In EF8 questa query genera il codice SQL seguente quando si usa il provider di database SQL di Azure:

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedBy] nvarchar(max) '$.UpdatedBy',
        [UpdatedOn] date '$.UpdatedOn',
        [Commits] nvarchar(max) '$.Commits' AS JSON
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

Si noti che e UpdatedByCommits non sono necessari in questa query. A partire da EF9, queste colonne vengono ora eliminati:

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedOn] date '$.UpdatedOn'
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

In alcuni scenari, ciò comporta la rimozione completa della WITH clausola . Ad esempio:

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

In EF8 questa query viene convertita nel codice SQL seguente:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

In EF9, questo è stato migliorato per:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

Traduzioni che coinvolgono GREATEST/LEAST

Suggerimento

Il codice illustrato di seguito proviene da LeastGreatestSample.cs.

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

Importante

Le GREATEST funzioni e LEAST 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 di 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:

info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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 per i valori dei parametri. 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 EF.Constant metodo 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 id valore :

info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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

EF9 introduce il EF.Parameter metodo 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":

info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      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

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 l'oggetto IQueryabledotnetPosts in è inlined, con conseguente singolo round trip:

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

Nuovi ToHashSetAsync<T> metodi

Suggerimento

Il codice illustrato di seguito proviene da QuerySample.cs.

I Enumerable.ToHashSet metodi sono esistenti a partire da .NET Core 2.0. In EF9 sono stati aggiunti i metodi asincroni equivalenti. Ad esempio:

var set1 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync();

var set2 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync(ReferenceEqualityComparer.Instance);

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

ExecuteUpdate e ExecuteDelete

Consentire il passaggio di istanze di tipi complessi a ExecuteUpdate

Suggerimento

Il codice illustrato di seguito proviene da ExecuteUpdateSample.cs.

L'API ExecuteUpdate è stata introdotta in EF7 per eseguire aggiornamenti immediati e diretti al database senza tenere traccia o SaveChanges. Ad esempio:

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Region, "Deutschland"));

L'esecuzione di questo codice esegue la query seguente per aggiornare l'oggetto Region in "Deutschland":

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

In EF8 ExecuteUpdate può essere usato anche per aggiornare i valori delle proprietà di tipo complesso. 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 == "Deutschland")
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
            .SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
            .SetProperty(b => b.StoreAddress.City, newAddress.City)
            .SetProperty(b => b.StoreAddress.Country, newAddress.Country)
            .SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));

L'esecuzione di questo codice comporta l'esecuzione della query seguente:

UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
    [s].[StoreAddress_Country] = @__newAddress_Country_3,
    [s].[StoreAddress_City] = @__newAddress_City_2,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'

In EF9 lo stesso aggiornamento può essere eseguito passando l'istanza di tipo complesso stessa. Ciò significa che non è necessario specificare in modo esplicito ogni membro. 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));

L'esecuzione di questo codice comporta la stessa esecuzione di query dell'esempio precedente:

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'

È possibile combinare più aggiornamenti a proprietà di tipo complesso e proprietà semplici in una singola chiamata a ExecuteUpdate. Ad esempio:

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(
                b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.Tag, "Tog"));

L'esecuzione di questo codice comporta la stessa esecuzione di query dell'esempio precedente:

UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
    [c].[CustomerInfo_HomeAddress_City] = N'Beetley',
    [c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
    [c].[CustomerInfo_HomeAddress_Line2] = NULL,
    [c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
    [c].[CustomerInfo_WorkAddress_City] = N'Beetley',
    [c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
    [c].[CustomerInfo_WorkAddress_Line2] = NULL,
    [c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

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

Compilazione di modelli

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 EFOptimizeContext proprietà sul .csproj file. 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 per true 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 altre informazioni, vedere Problema 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 tipate 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 nel modo normale. Ad esempio, questa query LINQ:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

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 la memorizzazione nella cache per le sequenze

Suggerimento

Il codice illustrato di seguito proviene da ModelBuildingSample.cs.

EF9 consente di impostare le opzioni di memorizzazione nella cache per le sequenze di database per qualsiasi provider di database relazionale che lo supporta. Ad esempio, UseCache può essere usato per attivare in modo esplicito la memorizzazione nella cache e impostare le dimensioni della cache:

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

Ciò comporta la definizione di sequenza seguente quando si usa SQL Server:

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

Analogamente, UseNoCache disattiva in modo esplicito la memorizzazione nella cache:

modelBuilder.HasSequence<int>("MyUncachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;

Se non viene chiamato né UseCacheUseNoCache vengono chiamati, la memorizzazione nella cache non viene specificata e il database userà qualsiasi valore predefinito. Può trattarsi di un valore predefinito diverso per database diversi.

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

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. Nella documentazione di SQL Server "Quando viene creato o ricompilato un indice, il valore del fattore di riempimento determina la percentuale di spazio in ogni pagina a livello foglia da riempire con i dati, riservando il resto in ogni pagina come spazio libero per una 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

In EF8 è stato aggiunto il supporto di prima classe per il tipo DI SQL ServerHierarchyId. 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 HierarchyId proprietà :

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

Questa HierarchyId proprietà 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/, che lo inserisce bteween 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.