Novità di EF Core 6.0

EF Core 6.0 è stato spedito a NuGet. Questa pagina contiene una panoramica delle modifiche interessanti introdotte in questa versione.

Suggerimento

È possibile eseguire ed eseguire il debug negli esempi illustrati di seguito scaricando il codice di esempio da GitHub.

Tabelle temporali di SQL Server

Problema di GitHub: #4693.

Le tabelle temporali di SQL Server tengono automaticamente traccia di tutti i dati mai archiviati in una tabella, anche dopo l'aggiornamento o l'eliminazione di tali dati. Ciò si ottiene creando una "tabella di cronologia" parallela in cui i dati cronologici con timestamp vengono archiviati ogni volta che viene apportata una modifica alla tabella principale. In questo modo è possibile eseguire query su dati cronologici, ad esempio per il controllo o il ripristino, ad esempio per il recupero dopo la mutazione o l'eliminazione accidentale.

EF Core supporta ora:

  • Creazione di tabelle temporali tramite Migrazioni
  • Trasformazione delle tabelle esistenti in tabelle temporali, usando di nuovo Le migrazioni
  • Esecuzione di query sui dati cronologici
  • Ripristino dei dati da qualche punto nel passato

Configurazione di una tabella temporale

Il generatore di modelli può essere usato per configurare una tabella come temporale. Ad esempio:

modelBuilder
    .Entity<Employee>()
    .ToTable("Employees", b => b.IsTemporal());

Quando si usa EF Core per creare il database, la nuova tabella verrà configurata come tabella temporale con le impostazioni predefinite di SQL Server per i timestamp e la tabella di cronologia. Si consideri ad esempio un Employee tipo di entità:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

La tabella temporale creata sarà simile alla seguente:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

Si noti che SQL Server crea due colonne nascoste datetime2 denominate PeriodEnd e PeriodStart. Queste "colonne periodo" rappresentano l'intervallo di tempo durante il quale sono presenti i dati nella riga. Queste colonne vengono mappate alle proprietà shadow nel modello EF Core, consentendo di usarle nelle query, come illustrato più avanti.

Importante

Le ore in queste colonne sono sempre l'ora UTC generata da SQL Server. Le ore UTC vengono usate per tutte le operazioni che coinvolgono tabelle temporali, ad esempio nelle query illustrate di seguito.

Si noti anche che viene creata automaticamente una tabella di cronologia associata denominata EmployeeHistory . I nomi delle colonne di periodo e della tabella di cronologia possono essere modificati con una configurazione aggiuntiva al generatore di modelli. Ad esempio:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        b => b.IsTemporal(
            b =>
            {
                b.HasPeriodStart("ValidFrom");
                b.HasPeriodEnd("ValidTo");
                b.UseHistoryTable("EmployeeHistoricalData");
            }));

Ciò si riflette nella tabella creata da SQL Server:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

Uso di tabelle temporali

Nella maggior parte dei casi, le tabelle temporali vengono usate esattamente come qualsiasi altra tabella. Ovvero, le colonne period e i dati cronologici vengono gestiti in modo trasparente da SQL Server in modo che l'applicazione possa ignorarle. Ad esempio, le nuove entità possono essere salvate nel database nel modo normale:

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

context.SaveChanges();

Questi dati possono quindi essere sottoposti a query, aggiornati ed eliminati nel modo normale. Ad esempio:

var employee = context.Employees.Single(e => e.Name == "Rainbow Dash");
context.Remove(employee);
context.SaveChanges();

Inoltre, dopo una normale query di rilevamento, è possibile accedere ai valori delle colonne periodo dei dati correnti dalle entità rilevate. Ad esempio:

var employees = context.Employees.ToList();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

Verrà stampato quanto segue:

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

Si noti che la ValidTo colonna (per impostazione predefinita denominata PeriodEnd) contiene il datetime2 valore massimo. Questo è sempre il caso per le righe correnti nella tabella. Le ValidFrom colonne (per impostazione predefinita denominate PeriodStart) contengono l'ora UTC in cui è stata inserita la riga.

Esecuzione di query sui dati cronologici

EF Core supporta query che includono dati cronologici tramite diversi nuovi operatori di query:

  • TemporalAsOf: restituisce righe attive (correnti) all'ora UTC specificata. Si tratta di una singola riga della tabella o della tabella di cronologia corrente per una determinata chiave primaria.
  • TemporalAll: restituisce tutte le righe nei dati cronologici. Si tratta in genere di molte righe della tabella di cronologia e/o della tabella corrente per una determinata chiave primaria.
  • TemporalFromTo: restituisce tutte le righe attive tra due ore UTC specificate. Può trattarsi di molte righe della tabella di cronologia e/o della tabella corrente per una determinata chiave primaria.
  • TemporalBetween: uguale a TemporalFromTo, ad eccezione del fatto che le righe sono incluse che sono diventate attive nel limite superiore.
  • TemporalContainedIn: restituisce tutte le righe che hanno iniziato ad essere attive e sono state attivate tra due ore UTC specificate. Può trattarsi di molte righe della tabella di cronologia e/o della tabella corrente per una determinata chiave primaria.

Nota

Per altre informazioni sulle righe incluse per ognuno di questi operatori, vedere la documentazione relativa alle tabelle temporali di SQL Server.

Ad esempio, dopo aver apportato alcuni aggiornamenti ed eliminati ai dati, è possibile eseguire una query usando TemporalAll per visualizzare i dati cronologici:

var history = context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

Si noti il modo in cui Entity Framework. Il metodo di proprietà può essere usato per accedere ai valori dalle colonne period. Viene usato nella OrderBy clausola per ordinare i dati e quindi in una proiezione per includere questi valori nei dati restituiti.

Questa query restituisce i dati seguenti:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

Si noti che l'ultima riga restituita non è più attiva alle ore 26/08/2021 4:44:59. Ciò è dovuto al fatto che la riga per Rainbow Dash è stata eliminata dalla tabella principale in quel momento. Più avanti si vedrà come ripristinare questi dati.

È possibile scrivere query simili usando TemporalFromTo, TemporalBetweeno TemporalContainedIn. Ad esempio:

var history = context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

Questa query restituisce le righe seguenti:

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

Ripristino dei dati cronologici

Come accennato in precedenza, Rainbow Dash è stato eliminato dalla Employees tabella. Questo è stato chiaramente un errore, quindi torniamo a un punto nel tempo e ripristina la riga mancante da quel momento.

var employee = context
    .Employees
    .TemporalAsOf(timeStamp2)
    .Single(e => e.Name == "Rainbow Dash");

context.Add(employee);
context.SaveChanges();

Questa query restituisce una singola riga per Rainbow Dash così come era all'ora UTC specificata. Tutte le query che usano operatori temporali non sono tracciate per impostazione predefinita, quindi l'entità restituita non viene rilevata. Questo ha senso, perché attualmente non esiste nella tabella principale. Per reinserire l'entità nella tabella principale, è sufficiente contrassegnarla come Added e quindi chiamare SaveChanges.

Dopo aver reinserito la riga Rainbow Dash, l'esecuzione di query sui dati cronologici mostra che la riga è stata ripristinata come era all'ora UTC specificata:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

Bundle di migrazione

Problema di GitHub: #19693.

Le migrazioni di EF Core vengono usate per generare aggiornamenti dello schema del database in base alle modifiche apportate al modello di Entity Framework. Questi aggiornamenti dello schema devono essere applicati in fase di distribuzione dell'applicazione, spesso come parte di un sistema di integrazione continua/distribuzione continua (C.I./C.D.).

EF Core include ora un nuovo modo per applicare questi aggiornamenti dello schema: bundle di migrazione. Un bundle di migrazione è un file eseguibile di piccole dimensioni contenente le migrazioni e il codice necessario per applicare queste migrazioni al database.

Nota

Per una discussione più approfondita sulle migrazioni, i bundle e le distribuzioni di EF Core compatibili con DevOps, vedere Il blog di .NET illustra in modo più approfondito le migrazioni, i bundle e la distribuzione.

I bundle di migrazione vengono creati usando lo strumento da dotnet ef riga di comando. Assicurarsi di aver installato la versione più recente dello strumento prima di continuare.

Un bundle richiede migrazioni da includere. Questi vengono creati usando dotnet ef migrations add come descritto nella documentazione delle migrazioni. Dopo aver pronto la distribuzione delle migrazioni, creare un bundle usando .dotnet ef migrations bundle Ad esempio:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

L'output è un eseguibile adatto per il sistema operativo di destinazione. Nel mio caso si tratta di Windows x64, quindi viene eliminato efbundle.exe nella cartella locale. L'esecuzione di questo eseguibile applica le migrazioni contenute al suo interno:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Le migrazioni vengono applicate al database solo se non sono già state applicate. Ad esempio, l'esecuzione dello stesso bundle non esegue di nuovo alcuna operazione, poiché non sono disponibili nuove migrazioni da applicare:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>

Tuttavia, se vengono apportate modifiche al modello e vengono generate più migrazioni con dotnet ef migrations add, questi possono essere raggruppati in un nuovo eseguibile pronto per l'applicazione. Ad esempio:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Si noti che l'opzione --force può essere usata per sovrascrivere il bundle esistente con uno nuovo.

L'esecuzione di questo nuovo bundle applica queste due nuove migrazioni al database:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Per impostazione predefinita, il bundle usa il database stringa di connessione dalla configurazione dell'applicazione. Tuttavia, è possibile eseguire la migrazione di un database diverso passando il stringa di connessione nella riga di comando. Ad esempio:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Si noti che questa volta sono state applicate tutte e tre le migrazioni, poiché nessuna di esse era ancora stata applicata al database di produzione.

È possibile passare altre opzioni alla riga di comando. Alcune opzioni comuni sono:

  • --output per specificare il percorso del file eseguibile da creare.
  • --context per specificare il tipo DbContext da utilizzare quando il progetto contiene più tipi di contesto.
  • --project per specificare il progetto da utilizzare. Il valore predefinito è la directory di lavoro corrente.
  • --startup-project per specificare il progetto di avvio da usare. Il valore predefinito è la directory di lavoro corrente.
  • --no-build per impedire la compilazione del progetto prima di eseguire il comando . Questa operazione deve essere utilizzata solo se il progetto è noto per essere aggiornato.
  • --verbose per visualizzare informazioni dettagliate sulle operazioni eseguite dal comando. Usare questa opzione quando si includono informazioni nei report sui bug.

Usare dotnet ef migrations bundle --help per visualizzare tutte le opzioni disponibili.

Si noti che per impostazione predefinita ogni migrazione viene applicata nella propria transazione. Per una descrizione dei possibili miglioramenti futuri in questa area, vedere il problema 22616 di GitHub.

Configurazione del modello pre-convenzione

Problema di GitHub: #12229.

Le versioni precedenti di EF Core richiedono che il mapping per ogni proprietà di un determinato tipo sia configurato in modo esplicito quando tale mapping è diverso da quello predefinito. Sono inclusi "facet" come la lunghezza massima delle stringhe e la precisione decimale, nonché la conversione di valori per il tipo di proprietà.

Ciò ha richiesto:

  • Configurazione del generatore di modelli per ogni proprietà
  • Attributo di mapping in ogni proprietà
  • Iterazione esplicita su tutte le proprietà di tutti i tipi di entità e uso delle API di metadati di basso livello durante la compilazione del modello.

Si noti che l'iterazione esplicita è soggetta a errori e difficile da eseguire in modo affidabile perché l'elenco di tipi di entità e le proprietà mappate potrebbe non essere finale al momento in cui si verifica questa iterazione.

EF Core 6.0 consente di specificare questa configurazione di mapping una sola volta per un determinato tipo. Verrà quindi applicato a tutte le proprietà di quel tipo nel modello. Questa operazione è denominata "configurazione del modello pre-convenzione", poiché configura gli aspetti del modello che vengono quindi usati dalle convenzioni di compilazione del modello. Tale configurazione viene applicata eseguendo l'override di ConfigureConventionsDbContextin :

public class SomeDbContext : DbContext
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        // Pre-convention model configuration goes here
    }
}

Si considerino ad esempio i tipi di entità seguenti:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

Tutte le proprietà stringa possono essere configurate in modo che siano ANSI (anziché Unicode) e abbiano una lunghezza massima di 1024:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

Tutte le proprietà DateTime possono essere convertite in numeri interi a 64 bit nel database, usando la conversione predefinita da DateTimes a longs:

configurationBuilder
    .Properties<DateTime>()
    .HaveConversion<long>();

Tutte le proprietà bool possono essere convertite in numeri interi 0 o 1 usando uno dei convertitori di valori predefiniti:

configurationBuilder
    .Properties<bool>()
    .HaveConversion<BoolToZeroOneConverter<int>>();

Supponendo che Session sia una proprietà temporanea dell'entità e non debba essere mantenuta, può essere ignorata ovunque nel modello:

configurationBuilder
    .IgnoreAny<Session>();

La configurazione del modello pre-convenzione è molto utile quando si utilizzano oggetti valore. Ad esempio, il tipo Money nel modello precedente è rappresentato da uno struct di sola lettura:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Viene quindi serializzato da e verso JSON usando un convertitore di valori personalizzato:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
    {
    }
}

Questo convertitore di valori può essere configurato una sola volta per tutti gli usi di Money:

configurationBuilder
    .Properties<Money>()
    .HaveConversion<MoneyConverter>()
    .HaveMaxLength(64);

Si noti anche che è possibile specificare facet aggiuntivi per la colonna stringa in cui è archiviato il codice JSON serializzato. In questo caso, la colonna è limitata a una lunghezza massima di 64.

Le tabelle create per SQL Server usando le migrazioni mostrano come è stata applicata la configurazione a tutte le colonne mappate:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

È anche possibile specificare un mapping dei tipi predefinito per un determinato tipo. Ad esempio:

configurationBuilder
    .DefaultTypeMapping<string>()
    .IsUnicode(false);

Questa operazione è raramente necessaria, ma può essere utile se un tipo viene usato nella query in modo non correlato a qualsiasi proprietà mappata del modello.

Nota

Vedere Annuncio di Entity Framework Core 6.0 Preview 6: Configure Conventions (Annuncio di Entity Framework Core 6.0 Preview 6: Configurare convenzioni ) nel blog di .NET per altre informazioni ed esempi di configurazione del modello pre-convenzione.

Modelli compilati

Problema di GitHub: #1906.

I modelli compilati possono migliorare il tempo di avvio di EF Core per le applicazioni con modelli di grandi dimensioni. Un modello di grandi dimensioni indica in genere da 100 a 1000 di tipi di entità e relazioni.

Ora di avvio indica il tempo necessario per eseguire la prima operazione in un oggetto DbContext quando il tipo DbContext viene usato per la prima volta nell'applicazione. Si noti che solo la creazione di un'istanza DbContext non comporta l'inizializzazione del modello ef. Al contrario, le prime operazioni tipiche che causano l'inizializzazione del modello includono la chiamata DbContext.Add o l'esecuzione della prima query.

I modelli compilati vengono creati usando lo strumento da dotnet ef riga di comando. Assicurarsi di aver installato la versione più recente dello strumento prima di continuare.

Viene usato un nuovo dbcontext optimize comando per generare il modello compilato. Ad esempio:

dotnet ef dbcontext optimize

Le --output-dir opzioni e --namespace possono essere usate per specificare la directory e lo spazio dei nomi in cui verrà generato il modello compilato. Ad esempio:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

L'output dell'esecuzione di questo comando include una parte di codice da copiare e incollare nella configurazione DbContext per fare in modo che EF Core usi il modello compilato. Ad esempio:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Bootstrap del modello compilato

In genere non è necessario esaminare il codice di bootstrap generato. Tuttavia, a volte può essere utile personalizzare il modello o il relativo caricamento. Il codice di bootstrapping è simile al seguente:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

Si tratta di una classe parziale con metodi parziali che possono essere implementati per personalizzare il modello in base alle esigenze.

Inoltre, è possibile generare più modelli compilati per i tipi DbContext che possono usare modelli diversi a seconda di una configurazione di runtime. Questi elementi devono essere inseriti in cartelle e spazi dei nomi diversi, come illustrato in precedenza. Le informazioni di runtime, ad esempio il stringa di connessione, possono quindi essere esaminate e il modello corretto restituito in base alle esigenze. Ad esempio:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

Limiti

I modelli compilati presentano alcune limitazioni:

A causa di queste limitazioni, è consigliabile usare modelli compilati solo se il tempo di avvio di EF Core è troppo lento. La compilazione di modelli di piccole dimensioni in genere non ne vale la pena.

Se il supporto di una di queste funzionalità è fondamentale per il successo, votare per i problemi appropriati collegati in precedenza.

Benchmark

Suggerimento

È possibile provare a compilare un modello di grandi dimensioni ed eseguire un benchmark su di esso scaricando il codice di esempio da GitHub.

Il modello nel repository GitHub a cui si fa riferimento sopra contiene 449 tipi di entità, 6390 proprietà e 720 relazioni. Si tratta di un modello moderatamente grande. Usando BenchmarkDotNet per misurare, il tempo medio per la prima query è di 1,02 secondi su un portatile ragionevolmente potente. L'uso di modelli compilati comporta un calo di 117 millisecondi sullo stesso hardware. Un miglioramento da 8x a 10x come questo rimane relativamente costante man mano che le dimensioni del modello aumentano.

Compiled model performance improvement

Nota

Per una descrizione più approfondita delle prestazioni di avvio e dei modelli compilati, vedere Annuncio di Entity Framework Core 6.0 Preview 5: Modelli compilati nel blog .NET.

Miglioramento delle prestazioni su TechEmpower Fortunes

Problema di GitHub: #23611.

Sono stati apportati miglioramenti significativi alle prestazioni delle query per EF Core 6.0. In particolare:

  • Le prestazioni di EF Core 6.0 sono ora del 70% più veloci rispetto al benchmark TechEmpower Fortunes standard del settore, rispetto al 5,0.
    • Si tratta del miglioramento delle prestazioni dello stack completo, inclusi i miglioramenti nel codice di benchmark, il runtime .NET e così via.
  • EF Core 6.0 è il 31% più veloce nell'esecuzione di query non rilevate.
  • Le allocazioni dell'heap sono state ridotte del 43% durante l'esecuzione di query.

Dopo questi miglioramenti, il divario tra il popolare "micro-ORM" Dapper e EF Core nel benchmark TechEmpower Fortunes si è ridotto dal 55% a circa il 55%.

Nota

Vedere Annuncio di Entity Framework Core 6.0 Preview 4: Performance Edition nel blog di .NET per una descrizione dettagliata dei miglioramenti delle prestazioni delle query in EF Core 6.0.

Miglioramenti del provider Azure Cosmos DB

EF Core 6.0 contiene molti miglioramenti al provider di database Azure Cosmos DB.

Suggerimento

È possibile eseguire ed eseguire il debug in tutti gli esempi specifici di Cosmos scaricando il codice di esempio da GitHub.

Per impostazione predefinita, la proprietà implicita

Problema di GitHub: #24803.

Quando si compila un modello per il provider Azure Cosmos DB, EF Core 6.0 contrassegnerà i tipi di entità figlio come di proprietà dell'entità padre per impostazione predefinita. In questo modo si elimina la necessità di gran parte delle OwnsMany chiamate e OwnsOne nel modello di Azure Cosmos DB. In questo modo è più semplice incorporare i tipi figlio nel documento per il tipo padre, che in genere è il modo appropriato per modellare elementi padre e figli in un database di documenti.

Si considerino ad esempio questi tipi di entità:

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }
    
    public string LastName { get; set; }
    public bool IsRegistered { get; set; }
    
    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

In EF Core 5.0 questi tipi sarebbero stati modellati per Azure Cosmos DB con la configurazione seguente:

modelBuilder.Entity<Family>()
    .HasPartitionKey(e => e.LastName)
    .OwnsMany(f => f.Parents);

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);        

In EF Core 6.0 la proprietà è implicita, riducendo la configurazione del modello in:

modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);

I documenti di Azure Cosmos DB risultanti includono i genitori, i figli, gli animali domestici e l'indirizzo della famiglia incorporati nel documento della famiglia. Ad esempio:

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

Nota

È importante ricordare che la OwnsOne/OwnsMany configurazione deve essere usata se è necessario configurare ulteriormente questi tipi di proprietà.

Raccolte di tipi primitivi

Problema di GitHub: #14762.

EF Core 6.0 esegue il mapping nativo delle raccolte di tipi primitivi quando si usa il provider di database Azure Cosmos DB. Si consideri ad esempio questo tipo di entità:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Sia l'elenco che il dizionario possono essere popolati e inseriti nel database in modo normale:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

Si ottiene come risultato il documento JSON seguente:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Queste raccolte possono quindi essere aggiornate, sempre in modo normale:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

Limitazioni :

  • Sono supportati solo i dizionari con chiavi stringa
  • L'esecuzione di query nel contenuto delle raccolte primitive non è attualmente supportata. Se si ritiene che queste funzionalità siano importanti, votare per #16926, #25700 e #25701.

Traduzioni in funzioni predefinite

Problema di GitHub: #16143.

Il provider Azure Cosmos DB ora converte più metodi BCL (Base Class Library) in funzioni predefinite di Azure Cosmos DB. Le tabelle seguenti illustrano le traduzioni nuove in EF Core 6.0.

Traduzioni di stringhe

BCL, metodo Funzione predefinita Note
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ Operatore CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL Solo chiamate senza distinzione tra maiuscole e minuscole

Le traduzioni per LOWER, LTRIM, RTRIMTRIM, , UPPERe SUBSTRING sono state fornite da @Marusyk. Grazie mille!

Ad esempio:

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

Che si traduce in:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

Traduzioni matematiche

BCL, metodo Funzione predefinita
Math.Abs oppure MathF.Abs ABS
Math.Acos oppure MathF.Acos ACOS
Math.Asin oppure MathF.Asin ASIN
Math.Atan oppure MathF.Atan ATAN
Math.Atan2 oppure MathF.Atan2 ATN2
Math.Ceiling oppure MathF.Ceiling CEILING
Math.Cos oppure MathF.Cos COS
Math.Exp oppure MathF.Exp EXP
Math.Floor oppure MathF.Floor FLOOR
Math.Log oppure MathF.Log LOG
Math.Log10 oppure MathF.Log10 LOG10
Math.Pow oppure MathF.Pow POWER
Math.Round oppure MathF.Round ROUND
Math.Sign oppure MathF.Sign SIGN
Math.Sin oppure MathF.Sin SIN
Math.Sqrt oppure MathF.Sqrt SQRT
Math.Tan oppure MathF.Tan TAN
Math.Truncate oppure MathF.Truncate TRUNC
DbFunctions.Random RAND

Queste traduzioni sono state fornite da @Marusyk. Grazie mille!

Ad esempio:

var hypotenuse = 42.42;
var mathResults = await context.Triangles.Where(
        e => (Math.Round(e.Angle1) == 90.0
              || Math.Round(e.Angle2) == 90.0)
             && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                 || hypotenuse * Math.Cos(e.Angle2) > 30.0))
    .ToListAsync();

Che si traduce in:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

Traduzioni DateTime

BCL, metodo Funzione predefinita
DateTime.UtcNow GetCurrentDateTime

Queste traduzioni sono state fornite da @Marusyk. Grazie mille!

Ad esempio:

var timeResults = await context.Triangles.Where(
        e => e.InsertedOn <= DateTime.UtcNow)
    .ToListAsync();

Che si traduce in:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))

Query SQL non elaborate con FromSql

Problema di GitHub: #17311.

A volte è necessario eseguire una query SQL non elaborata anziché usare LINQ. Questa funzionalità è ora supportata con il provider Azure Cosmos DB tramite l'uso del FromSql metodo . Questo funziona allo stesso modo in cui ha sempre fatto con i provider relazionali. Ad esempio:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .ToListAsync();

Che viene eseguito come:

SELECT c
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c

Query distinte

Problema di GitHub: #16144.

Le query semplici che usano Distinct vengono ora tradotte. Ad esempio:

var distinctResults = await context.Triangles
    .Select(e => e.Angle1).OrderBy(e => e).Distinct()
    .ToListAsync();

Che si traduce in:

SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]

Diagnostica

Problema di GitHub: #17298.

Il provider Azure Cosmos DB registra ora altre informazioni di diagnostica, inclusi gli eventi per l'inserimento, l'esecuzione di query, l'aggiornamento e l'eliminazione di dati dal database. Le unità richiesta (UR) sono incluse in questi eventi ogni volta che sono appropriate.

Nota

I log mostrano qui l'uso EnableSensitiveDataLogging() in modo che vengano visualizzati i valori ID.

L'inserimento di un elemento nel database Azure Cosmos DB genera l'evento CosmosEventId.ExecutedCreateItem . Ad esempio, questo codice:

var triangle = new Triangle
{
    Name = "Impossible",
    PartitionKey = "TrianglesPartition",
    Angle1 = 90,
    Angle2 = 90,
    InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
context.SaveChanges();

Registra l'evento di diagnostica seguente:

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Il recupero di elementi dal database Di Azure Cosmos DB tramite una query genera l'evento CosmosEventId.ExecutingSqlQuery e quindi uno o più CosmosEventId.ExecutedReadNext eventi per gli elementi letti. Ad esempio, questo codice:

var equilateral = context.Triangles.Single(e => e.Name == "Equilateral");

Registra gli eventi di diagnostica seguenti:

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

Il recupero di un singolo elemento dal database Azure Cosmos DB tramite Find con una chiave di partizione genera gli CosmosEventId.ExecutingReadItem eventi e CosmosEventId.ExecutedReadItem . Ad esempio, questo codice:

var isosceles = context.Triangles.Find("Isosceles", "TrianglesPartition");

Registra gli eventi di diagnostica seguenti:

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

Il salvataggio di un elemento aggiornato nel database di Azure Cosmos DB genera l'evento CosmosEventId.ExecutedReplaceItem . Ad esempio, questo codice:

triangle.Angle2 = 89;
context.SaveChanges();

Registra l'evento di diagnostica seguente:

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

L'eliminazione di un elemento dal database di Azure Cosmos DB genera l'evento CosmosEventId.ExecutedDeleteItem . Ad esempio, questo codice:

context.Remove(triangle);
context.SaveChanges();

Registra l'evento di diagnostica seguente:

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Configurare la velocità effettiva

Problema di GitHub: #17301.

Il modello di Azure Cosmos DB può ora essere configurato con velocità effettiva manuale o scalabilità automatica. Questi valori effettuano il provisioning della velocità effettiva nel database. Ad esempio:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

Inoltre, è possibile configurare singoli tipi di entità per effettuare il provisioning della velocità effettiva per il contenitore corrispondente. Ad esempio:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Configurare la durata (TTL)

Problema di GitHub: #17307.

I tipi di entità nel modello di Azure Cosmos DB possono ora essere configurati con la durata predefinita e la durata per l'archivio analitico. Ad esempio:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasDefaultTimeToLive(100);
        entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
    });

Risolvere la factory del client HTTP

Problema di GitHub: #21274. Questa funzionalità è stata fornita da @dnperfors. Grazie mille!

L'oggetto HttpClientFactory usato dal provider Azure Cosmos DB ora può essere impostato in modo esplicito. Ciò può essere particolarmente utile durante i test, ad esempio per ignorare la convalida dei certificati quando si usa l'emulatore di Azure Cosmos DB in Linux:

optionsBuilder
    .EnableSensitiveDataLogging()
    .UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        "PrimitiveCollections",
        cosmosOptionsBuilder =>
        {
            cosmosOptionsBuilder.HttpClientFactory(
                () => new HttpClient(
                    new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback =
                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    }));
        });

Nota

Per un esempio dettagliato dell'applicazione dei miglioramenti apportati al provider Azure Cosmos DB per un'applicazione esistente, vedere Uso del provider Azure Cosmos DB per un test drive .

Miglioramenti allo scaffolding da un database esistente

EF Core 6.0 contiene diversi miglioramenti durante la reverse engineering di un modello ef da un database esistente.

Scaffolding di relazioni molti-a-molti

Problema di GitHub: #22475.

EF Core 6.0 rileva tabelle di join semplici e genera automaticamente un mapping molti-a-molti. Si considerino, ad esempio, le tabelle per Posts e Tagse una tabella PostTag join che le connettono:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

È possibile eseguire lo scaffolding di queste tabelle dalla riga di comando. Ad esempio:

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer

Ciò comporta una classe per Post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

E una classe per Tag:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Ma nessuna classe per la PostTag tabella. Viene invece eseguito lo scaffolding della configurazione per una relazione molti-a-molti:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Scaffolding dei tipi di riferimento nullable C#

Problema di GitHub: #15520.

EF Core 6.0 esegue ora lo scaffolding di un modello ef e dei tipi di entità che usano tipi riferimento nullable C# (NRT). L'utilizzo di NRT viene eseguito automaticamente quando il supporto NRT è abilitato nel progetto C# in cui viene eseguito lo scaffolding del codice.

Ad esempio, la tabella seguente Tags contiene entrambe le colonne stringa nullable non nullable:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

Ciò comporta le proprietà stringa nullable e non nullable corrispondenti nella classe generata:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Analogamente, le tabelle seguenti Posts contengono una relazione obbligatoria con la Blogs tabella :

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

Ciò comporta lo scaffolding della relazione non nullable (obbligatoria) tra i blog:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

E post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Infine, le proprietà DbSet nel DbContext generato vengono create in modo descrittivo per NRT. Ad esempio:

public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;

I commenti del database vengono scaffolding nei commenti di codice

Problema di GitHub: #19113. Questa funzionalità è stata fornita da @ErikEJ. Grazie mille!

I commenti sulle tabelle e le colonne SQL vengono ora scaffolding nei tipi di entità creati durante la reverse engineering di un modello EF Core da un database SQL Server esistente.

/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
    /// <summary>
    /// The primary key.
    /// </summary>
    [Key]
    public int Id { get; set; }
}

Miglioramenti delle query LINQ

EF Core 6.0 contiene diversi miglioramenti nella conversione e nell'esecuzione di query LINQ.

Supporto di GroupBy migliorato

Problemi di GitHub: #12088, #13805 e #22609.

EF Core 6.0 contiene un supporto migliore per GroupBy le query. In particolare, EF Core ora:

  • Translate GroupBy seguito da FirstOrDefault (o simile) su un gruppo
  • Supporta la selezione dei primi N risultati da un gruppo
  • Espande gli spostamenti dopo l'applicazione dell'operatore GroupBy

Di seguito sono riportate query di esempio dai report dei clienti e la relativa traduzione in SQL Server.

Esempio 1:

var people = context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToList();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

Esempio 2:

var group = context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .First();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

Esempio 3:

var people = context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

Esempio 4:

var results = (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToList();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

Esempio 5:

var results = context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

Esempio 6:

var results = context.People.Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

Esempio 7:

var size = 11;
var results
    = context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToList();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

Esempio 8:

var result = context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .Count();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

Esempio 9:

var results = context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToList();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

Esempio 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

Esempio 11:

var grouping = context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToList();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

Esempio 12:

var grouping = context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToList();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

Esempio 13:

var grouping = context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToList();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

Modello

I tipi di entità usati per questi esempi sono:

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

Tradurre String.Concat con più argomenti

Problema di GitHub: #23859. Questa funzionalità è stata fornita da @wmeints. Grazie mille!

A partire da EF Core 6.0, le chiamate a String.Concat con più argomenti vengono ora convertite in SQL. Ad esempio, la query seguente:

var shards = context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToList();

Verrà convertito nel codice SQL seguente quando si usa SQL Server:

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

Integrazione più fluida con System.Linq.Async

Problema di GitHub: #24041.

Il pacchetto System.Linq.Async aggiunge l'elaborazione LINQ asincrona lato client. L'uso di questo pacchetto con le versioni precedenti di EF Core era complesso a causa di uno scontro dello spazio dei nomi per i metodi LINQ asincroni. In EF Core 6.0 sono stati sfruttati i criteri di ricerca C# per IAsyncEnumerable<T> in modo che EF Core DbSet<TEntity> esposto non debba implementare direttamente l'interfaccia.

Si noti che la maggior parte delle applicazioni non deve usare System.Linq.Async perché le query EF Core vengono in genere convertite completamente nel server.

Problema di GitHub: #23921.

In EF Core 6.0 sono stati soddisfatti i requisiti dei parametri per FreeText(DbFunctions, String, String) e Contains. In questo modo queste funzioni possono essere usate con colonne binarie o con colonne mappate usando un convertitore di valori. Si consideri ad esempio un tipo di entità con una Name proprietà definita come oggetto valore:

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

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

Viene eseguito il mapping a JSON nel database:

modelBuilder.Entity<Customer>()
    .Property(e => e.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));

È ora possibile eseguire una query usando Contains o FreeText anche se il tipo della proprietà non stringè Name . Ad esempio:

var result = context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToList();

In questo modo viene generato il codice SQL seguente quando si usa SQL Server:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')

Translate ToString in SQLite

Problema di GitHub: #17223. Questa funzionalità è stata fornita da @ralmsdeveloper. Grazie mille!

Le chiamate a ToString() vengono ora convertite in SQL quando si usa il provider di database SQLite. Ciò può essere utile per le ricerche di testo che coinvolgono colonne non stringa. Si consideri ad esempio un User tipo di entità che archivia i numeri di telefono come valori numerici:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public long PhoneNumber { get; set; }
}

ToString può essere usato per convertire il numero in una stringa nel database. È quindi possibile usare questa stringa con una funzione, LIKE ad esempio per trovare numeri che corrispondono a un criterio. Ad esempio, per trovare tutti i numeri contenenti 555:

var users = context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToList();

Ciò si traduce nel codice SQL seguente quando si usa un database SQLite:

SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'

Si noti che la traduzione di ToString() per SQL Server è già supportata in EF Core 5.0 e può essere supportata anche da altri provider di database.

EF. Functions.Random

Problema di GitHub: #16141. Questa funzionalità è stata fornita da @RaymondHuy. Grazie mille!

EF.Functions.Random esegue il mapping a una funzione di database che restituisce un numero pseudo-casuale compreso tra 0 e 1 esclusivo. Le traduzioni sono state implementate nel repository EF Core per SQL Server, SQLite e Azure Cosmos DB. Si consideri ad esempio un User tipo di entità con una Popularity proprietà :

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public int Popularity { get; set; }
}

Popularity può avere valori compresi tra 1 e 5. Usando EF.Functions.Random è possibile scrivere una query per restituire tutti gli utenti con una popolarità scelta in modo casuale:

var users = context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToList();

Ciò si traduce nel codice SQL seguente quando si usa un database di SQL Server:

SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 4.0E0) AS int) + 1)

Miglioramento della traduzione di SQL Server per IsNullOrWhitespace

Problema di GitHub: #22916. Questa funzionalità è stata fornita da @Marusyk. Grazie mille!

Si consideri la query seguente:

var users = context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToList();

Prima di EF Core 6.0, questo è stato convertito nel codice seguente in SQL Server:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

Questa traduzione è stata migliorata per EF Core 6.0 per:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

Definizione della query per il provider in memoria

Problema di GitHub: #24600.

È possibile usare un nuovo metodo ToInMemoryQuery per scrivere una query di definizione sul database in memoria per un determinato tipo di entità. Ciò è particolarmente utile per creare l'equivalente delle viste nel database in memoria, soprattutto quando tali viste restituiscono tipi di entità senza chiave. Si consideri, ad esempio, un database del cliente per i clienti con sede nel Regno Unito. Ogni cliente ha un indirizzo:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Si supponga ora di voler visualizzare questi dati che mostrano il numero di clienti presenti in ogni area del codice postale. Per rappresentare questo tipo di entità, è possibile creare un tipo di entità senza chiave:

public class CustomerDensity
{
    public string Postcode { get; set; }
    public int CustomerCount { get; set; }
}

Definire una proprietà DbSet per tale proprietà in DbContext, insieme ai set per altri tipi di entità di primo livello:

public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }

OnModelCreatingIn è quindi possibile scrivere una query LINQ che definisce i dati da restituire per CustomerDensities:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

È quindi possibile eseguire query come qualsiasi altra proprietà DbSet:

var results = context.CustomerDensities.ToList();

Tradurre la sottostringa con un singolo parametro

Problema di GitHub: #20173. Questa funzionalità è stata fornita da @stevendarby. Grazie mille!

EF Core 6.0 ora converte gli usi di string.Substring con un singolo argomento. Ad esempio:

var result = context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefault(a => a.Name == "hur");

Ciò si traduce nel codice SQL seguente quando si usa SQL Server:

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

Query suddivise per raccolte non di spostamento

Problema di GitHub: #21234.

EF Core supporta la suddivisione di una singola query LINQ in più query SQL. In EF Core 6.0 questo supporto è stato ampliato per includere i casi in cui le raccolte non di spostamento sono contenute nella proiezione di query.

Di seguito sono riportate query di esempio che mostrano la conversione in SQL Server in una singola query o in più query.

Esempio 1:

Query LINQ:

context.Customers
    .Select(
        c => new
        {
            c,
            Orders = c.Orders
                .Where(o => o.Id > 1)
        })
    .ToList();

Query SQL singola:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Più query SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Esempio 2:

Query LINQ:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
        })
    .ToList();

Query SQL singola:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Più query SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Esempio 3:

Query LINQ:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
                .Distinct()
        })
    .ToList();

Query SQL singola:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Più query SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Rimuovere l'ultima clausola ORDER BY durante l'unione per la raccolta

Problema di GitHub: #19828.

Quando si caricano entità uno-a-molti correlate, EF Core aggiunge clausole ORDER BY per assicurarsi che tutte le entità correlate per una determinata entità siano raggruppate. Tuttavia, l'ultima clausola ORDER BY non è necessaria per generare i raggruppamenti necessari e può avere un impatto sulle prestazioni. Di conseguenza, EF Core 6.0 questa clausola viene rimossa.

Ad esempio, si consideri la query seguente:

context.Customers
    .Select(
        e => new
        {
            e.Id,
            FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
        })
    .ToList();

Con EF Core 5.0 in SQL Server, questa query viene convertita in:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

Con EF Core 6.0, viene invece convertito in:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Contrassegna le query con nome file e numero di riga

Problema di GitHub: #14176. Questa funzionalità è stata fornita da @michalczerwinski. Grazie mille!

I tag di query consentono di aggiungere un tag textural a una query LINQ in modo che venga quindi incluso in SQL generato. In EF Core 6.0 questo può essere usato per contrassegnare le query con il nome file e il numero di riga del codice LINQ. Ad esempio:

var results1 = context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToList();

Questo risultato è il codice SQL generato seguente quando si usa SQL Server:

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

Modifiche alla gestione dipendente facoltativa di proprietà

Problema di GitHub: #24558.

Diventa difficile sapere se un'entità dipendente facoltativa esiste o meno quando condivide una tabella con la relativa entità principale. Ciò è dovuto al fatto che nella tabella è presente una riga per il dipendente perché l'entità lo richiede, indipendentemente dal fatto che esista o meno l'entità dipendente. Il modo per gestire questa operazione in modo non ambiguo consiste nel garantire che il dipendente disponga di almeno una proprietà obbligatoria. Poiché una proprietà obbligatoria non può essere Null, significa che se il valore nella colonna per tale proprietà è Null, l'entità dipendente non esiste.

Si consideri, ad esempio, una classe in cui ogni cliente ha una Customer proprietà Address:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

L'indirizzo è facoltativo, vale a dire che è valido salvare un cliente senza indirizzo:

context.Customers1.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

Tuttavia, se un cliente ha un indirizzo, tale indirizzo deve avere almeno un codice postale non Null:

context.Customers1.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
        {
            Postcode = "AN1 1PL",
        }
    });

Questa operazione viene garantita contrassegnando la Postcode proprietà come Required.

Quando i clienti vengono sottoposti a query, se la colonna Postcode è null, significa che il cliente non ha un indirizzo e la Customer.Address proprietà di navigazione rimane null. Ad esempio, scorrere i clienti e verificare se l'indirizzo è Null:

foreach (var customer in context.Customers1)
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

Genera i risultati seguenti:

Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.

Si consideri invece il caso in cui non sia richiesta alcuna proprietà dall'indirizzo:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

È ora possibile salvare un cliente senza indirizzo e un cliente con un indirizzo in cui tutte le proprietà dell'indirizzo sono null:

context.Customers2.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

context.Customers2.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
    });

Tuttavia, nel database questi due casi sono indistinguibili, come si può vedere eseguendo direttamente query sulle colonne del database:

Id  Name               House   Street  City    Postcode
1   Foul Ole Ron       NULL    NULL    NULL    NULL
2   Havelock Vetinari  NULL    NULL    NULL    NULL

Per questo motivo, EF Core 6.0 avvisa ora quando si salva un dipendente facoltativo in cui tutte le relative proprietà sono Null. Ad esempio:

warn: 27/9/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) L'entità di tipo 'Address' con valori di chiave primaria {CustomerId: -2147482646} è un dipendente facoltativo tramite la condivisione di tabelle. L'entità non dispone di alcuna proprietà con un valore non predefinito per identificare se l'entità esiste. Ciò significa che quando viene eseguita una query su nessuna istanza di oggetto verrà creata anziché un'istanza con tutte le proprietà impostate su valori predefiniti. Anche eventuali dipendenti annidati andranno persi. Non salvare alcuna istanza con solo valori predefiniti o contrassegnare lo spostamento in ingresso come richiesto nel modello.

Questo diventa ancora più difficile in cui il dipendente facoltativo agisce un'entità per un'ulteriore dipendenza facoltativa, mappata anche alla stessa tabella. Anziché solo avvisare, EF Core 6.0 non consente solo casi di dipendenti facoltativi annidati. Si consideri ad esempio il modello seguente, dove ContactInfo è di proprietà di Customer e Address è di proprietà di ContactInfo:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Ora se ContactInfo.Phone è Null, EF Core non creerà un'istanza di Address se la relazione è facoltativa, anche se l'indirizzo stesso potrebbe avere dati. Per questo tipo di modello, EF Core 6.0 genererà l'eccezione seguente:

System.InvalidOperationException: il tipo di entità 'ContactInfo' è un dipendente facoltativo tramite la condivisione di tabelle e contiene altre dipendenze senza alcuna proprietà non condivisa necessaria per identificare se l'entità esiste. Se tutte le proprietà nullable contengono un valore Null nel database, non verrà creata un'istanza dell'oggetto nella query, causando la perdita dei valori dipendenti annidati. Aggiungere una proprietà obbligatoria per creare istanze con valori Null per altre proprietà o contrassegnare lo spostamento in ingresso come necessario per creare sempre un'istanza.

La riga inferiore qui consiste nell'evitare il caso in cui un dipendente facoltativo può contenere tutti i valori di proprietà nullable e condivide una tabella con il relativo entità. Esistono tre modi semplici per evitare questo problema:

  1. Rendere obbligatorio l'oggetto dipendente. Ciò significa che l'entità dipendente avrà sempre un valore dopo la query, anche se tutte le relative proprietà sono Null.
  2. Assicurarsi che il dipendente contenga almeno una proprietà obbligatoria, come descritto in precedenza.
  3. Salvare i dipendenti facoltativi nella propria tabella, anziché condividere una tabella con l'entità.

Un dipendente può essere reso obbligatorio usando l'attributo nel Required relativo spostamento:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

In alternativa, è necessario specificarlo in OnModelCreating:

modelBuilder.Entity<WithRequiredNavigation.Customer>(
    b =>
        {
            b.OwnsOne(e => e.Address);
            b.Navigation(e => e.Address).IsRequired();
        });

I dipendenti possono essere salvati in una tabella diversa specificando le tabelle da usare in OnModelCreating:

modelBuilder
    .Entity<WithDifferentTable.Customer>(
        b =>
            {
                b.ToTable("Customers");
                b.OwnsOne(
                    e => e.Address,
                    b => b.ToTable("CustomerAddresses"));
            });

Vedere OptionalDependentsSample in GitHub per altri esempi di dipendenti facoltativi, inclusi i casi con dipendenti facoltativi annidati.

Nuovi attributi di mapping

EF Core 6.0 contiene diversi nuovi attributi che possono essere applicati al codice per modificare la modalità di mapping al database.

UnicodeAttribute

Problema di GitHub: #19794. Questa funzionalità è stata fornita da @RaymondHuy. Grazie mille!

A partire da EF Core 6.0, è ora possibile eseguire il mapping di una proprietà stringa a una colonna non Unicode usando un attributo di mapping senza specificare direttamente il tipo di database. Si consideri ad esempio un Book tipo di entità con una proprietà per international standard book number (ISBN) nel formato "ISBN 978-3-16-148410-0":

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

Poiché gli ISBN non possono contenere caratteri non Unicode, l'attributo causerà l'utilizzo Unicode di un tipo stringa non Unicode. Viene inoltre MaxLength usato per limitare le dimensioni della colonna di database. Ad esempio, quando si usa SQL Server, viene generata una colonna di database di varchar(22):

CREATE TABLE [Book] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Isbn] varchar(22) NULL,
    CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));

Nota

EF Core esegue il mapping delle proprietà delle stringhe alle colonne Unicode per impostazione predefinita. UnicodeAttribute viene ignorato quando il sistema di database supporta solo i tipi Unicode.

PrecisionAttribute

Problema di GitHub: #17914. Questa funzionalità è stata fornita da @RaymondHuy. Grazie mille!

È ora possibile configurare la precisione e la scala di una colonna di database usando attributi di mapping senza specificare direttamente il tipo di database. Si consideri, ad esempio, un Product tipo di entità con una proprietà decimale Price :

public class Product
{
    public int Id { get; set; }

    [Precision(precision: 10, scale: 2)]
    public decimal Price { get; set; }
}

EF Core eseguirà il mapping di questa proprietà a una colonna di database con precisione 10 e scala 2. Ad esempio, in SQL Server:

CREATE TABLE [Product] (
    [Id] int NOT NULL IDENTITY,
    [Price] decimal(10,2) NOT NULL,
    CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));

EntityTypeConfigurationAttribute

Problema di GitHub: #23163. Questa funzionalità è stata fornita da @KaloyanIT. Grazie mille!

IEntityTypeConfiguration<TEntity> Le istanze consentono ModelBuilder la configurazione per ogni tipo di entità da contenere nella propria classe di configurazione. Ad esempio:

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder
            .Property(e => e.Isbn)
            .IsUnicode(false)
            .HasMaxLength(22);
    }
}

In genere, è necessario creare un'istanza di questa classe di configurazione e chiamare in da DbContext.OnModelCreating. Ad esempio:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}

A partire da EF Core 6.0, un EntityTypeConfigurationAttribute può essere inserito nel tipo di entità in modo che EF Core possa trovare e usare la configurazione appropriata. Ad esempio:

[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Isbn { get; set; }
}

Questo attributo significa che EF Core userà l'implementazione specificata IEntityTypeConfiguration ogni volta che il Book tipo di entità viene incluso in un modello. Il tipo di entità è incluso in un modello usando uno dei meccanismi normali. Ad esempio, creando una DbSet<TEntity> proprietà per il tipo di entità:

public class BooksContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    //...

In alternativa, registrandolo in OnModelCreating:

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

Nota

EntityTypeConfigurationAttribute I tipi non verranno individuati automaticamente in un assembly. I tipi di entità devono essere aggiunti al modello prima che l'attributo venga individuato in tale tipo di entità.

Miglioramenti alla creazione di modelli

Oltre ai nuovi attributi di mapping, EF Core 6.0 contiene diversi altri miglioramenti al processo di compilazione del modello.

Supporto per le colonne di tipo sparse di SQL Server

Problema di GitHub: #8023.

Le colonne di tipo sparse di SQL Server sono colonne normali ottimizzate per archiviare valori Null. Ciò può essere utile quando si usa il mapping di ereditarietà TPH in cui le proprietà di un sottotipo usato raramente genereranno valori di colonna Null per la maggior parte delle righe nella tabella. Si consideri, ad esempio, una ForumModerator classe che si estende da ForumUser:

public class ForumUser
{
    public int Id { get; set; }
    public string Username { get; set; }
}

public class ForumModerator : ForumUser
{
    public string ForumName { get; set; }
}

Ci possono essere milioni di utenti, con solo una manciata di questi moderatori. Ciò significa che il mapping di ForumName come sparse potrebbe avere senso qui. Questa operazione può ora essere configurata usando IsSparse in OnModelCreating. Ad esempio:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<ForumModerator>()
        .Property(e => e.ForumName)
        .IsSparse();
}

Le migrazioni di EF Core contrassegneranno quindi la colonna come sparse. Ad esempio:

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

Nota

Le colonne di tipo sparse presentano limitazioni. Assicurarsi di leggere la documentazione delle colonne di tipo sparse di SQL Server per assicurarsi che le colonne di tipo sparse siano la scelta giusta per lo scenario in uso.

Miglioramenti all'API HasConversion

Problema di GitHub: #25468.

Prima di EF Core 6.0, gli overload generici dei HasConversion metodi usavano il parametro generico per specificare il tipo in cui eseguire la conversione. Si consideri, ad esempio, un'enumerazione Currency :

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core può essere configurato per salvare i valori di questa enumerazione come stringhe "UsDollars", "PoundsStirling" e "Euros" usando HasConversion<string>. Ad esempio:

modelBuilder.Entity<TestEntity1>()
    .Property(e => e.Currency)
    .HasConversion<string>();

A partire da EF Core 6.0, il tipo generico può invece specificare un tipo di convertitore di valori. Può trattarsi di uno dei convertitori di valori predefiniti. Ad esempio, per archiviare i valori di enumerazione come numeri a 16 bit nel database:

modelBuilder.Entity<TestEntity2>()
    .Property(e => e.Currency)
    .HasConversion<EnumToNumberConverter<Currency, short>>();

Oppure può essere un tipo di convertitore di valori personalizzato. Si consideri, ad esempio, un convertitore che archivia i valori di enumerazione come simboli di valuta:

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

Questa operazione può ora essere configurata usando il metodo generico HasConversion :

modelBuilder.Entity<TestEntity3>()
    .Property(e => e.Currency)
    .HasConversion<CurrencyToSymbolConverter>();

Minore configurazione per relazioni molti-a-molti

Problema di GitHub: #21535.

Le relazioni molti-a-molti non ambigue tra due tipi di entità vengono individuate per convenzione. Se necessario o se necessario, gli spostamenti possono essere specificati in modo esplicito. Ad esempio:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats);

In entrambi questi casi EF Core crea un'entità condivisa tipizzata in base Dictionary<string, object> a per fungere da entità di join tra i due tipi. A partire da EF Core 6.0, UsingEntity può essere aggiunto alla configurazione per modificare solo questo tipo, senza la necessità di una configurazione aggiuntiva. Ad esempio:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>();

Inoltre, il tipo di entità join può essere configurato in modo aggiuntivo senza dover specificare in modo esplicito le relazioni a sinistra e a destra. Ad esempio:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

Infine, è possibile specificare la configurazione completa. Ad esempio:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
        e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

Consenti ai convertitori di valori di convertire i valori Null

Problema di GitHub: #13850.

Importante

A causa dei problemi descritti di seguito, i costruttori per ValueConverter che consentono la conversione di valori Null sono stati contrassegnati con [EntityFrameworkInternal] per la versione di EF Core 6.0. L'uso di questi costruttori genera ora un avviso di compilazione.

I convertitori di valori non consentono in genere la conversione di null in un altro valore. Ciò è dovuto al fatto che lo stesso convertitore di valori può essere usato sia per i tipi nullable che non nullable, che è molto utile per le combinazioni PK/FK in cui il valore FK è spesso nullable e l'infrastruttura a chiave pubblica non è null.

A partire da EF Core 6.0, è possibile creare un convertitore di valori che converte i valori Null. Tuttavia, la convalida di questa funzionalità ha rivelato che è stato molto problematico in pratica con molte insidie. Ad esempio:

Questi non sono problemi semplici e per i problemi di query non sono facili da rilevare. Di conseguenza, questa funzionalità è stata contrassegnata come interna per EF Core 6.0. È comunque possibile usarlo, ma verrà visualizzato un avviso del compilatore. L'avviso può essere disabilitato tramite #pragma warning disable EF1001.

Un esempio di dove la conversione di valori Null può essere utile è quando il database contiene valori Null, ma il tipo di entità vuole usare un altro valore predefinito per la proprietà. Si consideri, ad esempio, un'enumerazione in cui il valore predefinito è "Unknown":

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Tuttavia, il database può avere valori Null quando la razza è sconosciuta. In EF Core 6.0 è possibile usare un convertitore di valori per tenere conto di quanto segue:

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

I gatti con una razza di "Sconosciuto" avranno la colonna Breed impostata su Null nel database. Ad esempio:

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

context.SaveChanges();

Che genera le istruzioni insert seguenti in SQL Server:

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Miglioramenti della factory DbContext

AddDbContextFactory registra anche DbContext direttamente

Problema di GitHub: #25164.

In alcuni casi è utile avere sia un tipo DbContext che una factory per i contesti di tale tipo registrati sia nel contenitore D.I(Dependency Injection) delle applicazioni. Ciò consente, ad esempio, di risolvere un'istanza con ambito di DbContext dall'ambito della richiesta, mentre la factory può essere usata per creare più istanze indipendenti quando necessario.

Per supportare questa operazione, AddDbContextFactory ora registra anche il tipo DbContext come servizio con ambito. Si consideri ad esempio questa registrazione nel contenitore D.I. dell'applicazione:

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0"))
    .BuildServiceProvider();

Con questa registrazione, la factory può essere risolta dal contenitore D.I. radice, come nelle versioni precedenti:

var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
    // Contexts obtained from the factory must be explicitly disposed
}

Si noti che le istanze di contesto create dalla factory devono essere eliminate in modo esplicito.

Inoltre, un'istanza DbContext può essere risolta direttamente da un ambito contenitore:

using (var scope = container.CreateScope())
{
    var context = scope.ServiceProvider.GetService<SomeDbContext>();
    // Context is disposed when the scope is disposed
}

In questo caso l'istanza di contesto viene eliminata quando l'ambito del contenitore viene eliminato; il contesto non deve essere eliminato in modo esplicito.

A un livello superiore, ciò significa che il DbContext della factory può essere inserito in altri tipi D.I. Ad esempio:

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = context1.Blogs.ToList();
        var results2 = context2.Blogs.ToList();

        // Contexts obtained from the factory must be explicitly disposed
    }
}

Oppure:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public void DoSomething()
    {
        var results = _context.Blogs.ToList();

        // Injected context is disposed when the request scope is disposed
    }
}

DbContextFactory ignora il costruttore dbContext senza parametri

Problema di GitHub: #24124.

EF Core 6.0 ora consente sia un costruttore DbContext senza parametri che un costruttore che deve DbContextOptions essere usato nello stesso tipo di contesto quando la factory viene registrata tramite AddDbContextFactory. Ad esempio, il contesto usato negli esempi precedenti contiene entrambi i costruttori:

public class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

Il pooling DbContext può essere usato senza inserimento di dipendenze

Problema di GitHub: #24137.

Il PooledDbContextFactory tipo è stato reso pubblico in modo che possa essere usato come pool autonomo per le istanze DbContext, senza che l'applicazione abbia un contenitore di inserimento delle dipendenze. Il pool viene creato con un'istanza di che verrà usata per creare istanze di DbContextOptions contesto:

var options = new DbContextOptionsBuilder<SomeDbContext>()
    .EnableSensitiveDataLogging()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0")
    .Options;

var factory = new PooledDbContextFactory<SomeDbContext>(options);

La factory può quindi essere usata per creare e raggruppare istanze. Ad esempio:

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

Le istanze vengono restituite al pool quando vengono eliminate.

Miglioramenti vari

Infine, EF Core contiene diversi miglioramenti nelle aree non descritte in precedenza.

Usare [ColumnAttribute.Order] durante la creazione di tabelle

Problema di GitHub: #10059.

La Order proprietà di ColumnAttribute può ora essere utilizzata per ordinare le colonne durante la creazione di una tabella con migrazioni. Si consideri ad esempio il modello seguente:

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

Per impostazione predefinita, EF Core ordina prima le colonne chiave primaria, seguendo le proprietà del tipo di entità e dei tipi di proprietà e infine le proprietà dai tipi di base. Ad esempio, la tabella seguente viene creata in SQL Server:

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

In EF Core 6.0 ColumnAttribute è possibile usare per specificare un ordine di colonna diverso. Ad esempio:

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

In SQL Server la tabella generata è ora:

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

In questo modo le FistName colonne e LastName vengono spostate nella parte superiore, anche se sono definite in un tipo di base. Si noti che i valori dell'ordine di colonna possono avere spazi vuoti, consentendo l'uso di intervalli per posizionare sempre le colonne alla fine, anche se usate da più tipi derivati.

In questo esempio viene inoltre illustrato come usare lo stesso ColumnAttribute oggetto per specificare sia il nome della colonna che l'ordine.

L'ordinamento delle colonne può essere configurato anche usando l'API ModelBuilder in OnModelCreating. Ad esempio:

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

L'ordinamento nel generatore di modelli con HasColumnOrder ha la precedenza su qualsiasi ordine specificato con ColumnAttribute. Questo significa che HasColumnOrder può essere usato per eseguire l'override dell'ordinamento eseguito con attributi, inclusa la risoluzione di eventuali conflitti quando gli attributi in proprietà diverse specificano lo stesso numero di ordine.

Importante

Si noti che, nel caso generale, la maggior parte dei database supporta solo l'ordinamento delle colonne al momento della creazione della tabella. Ciò significa che l'attributo dell'ordine di colonna non può essere usato per riordinare le colonne in una tabella esistente. Un'eccezione rilevante è SQLite, in cui le migrazioni ricompilano l'intera tabella con nuovi ordini di colonna.

API minima di EF Core

Problema di GitHub: #25192.

.NET Core 6.0 include modelli aggiornati che includono API minime semplificate, che rimuovono molti dei codici boilerplate tradizionalmente necessari nelle applicazioni .NET.

EF Core 6.0 contiene un nuovo metodo di estensione che registra un tipo DbContext e fornisce la configurazione per un provider di database in una singola riga. Ad esempio:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCosmos<MyDbContext>(
    "https://localhost:8081",
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");

Questi sono esattamente equivalenti a:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));

Nota

Le API minime di EF Core supportano solo la registrazione e la configurazione di base di un dbContext e di un provider. Usare AddDbContext, AddDbContextPool, AddDbContextFactorye così via per accedere a tutti i tipi di registrazione e configurazione disponibili in EF Core.

Per altre informazioni sulle API minime, vedere queste risorse:

Mantenere il contesto di sincronizzazione in SaveChangesAsync

Problema di GitHub: #23971.

Il codice EF Core nella versione 5.0 è stato modificato in modo che sia impostato falseTask.ConfigureAwait su in tutte le posizioni in cui viene await definito il codice asincrono. Si tratta in genere di una scelta migliore per l'utilizzo di EF Core. Tuttavia, SaveChangesAsync è un caso speciale perché EF Core imposta i valori generati in entità rilevate al termine dell'operazione asincrona del database. Queste modifiche possono quindi attivare notifiche che, ad esempio, potrebbero dover essere eseguite nel thread U.I. Di conseguenza, questa modifica viene ripristinata solo in EF Core 6.0 per il SaveChangesAsync metodo .

Database in memoria: convalidare le proprietà obbligatorie non sono Null

Problema di GitHub: #10613. Questa funzionalità è stata fornita da @fagnercarvalho. Grazie mille!

Il database in memoria di EF Core genererà ora un'eccezione se viene effettuato un tentativo di salvare un valore Null per una proprietà contrassegnata come richiesta. Si consideri ad esempio un User tipo con una proprietà obbligatoria Username :

public class User
{
    public int Id { get; set; }

    [Required]
    public string Username { get; set; }
}

Il tentativo di salvare un'entità con un valore Null Username genererà l'eccezione seguente:

Microsoft.EntityFrameworkCore.DbUpdateException: proprietà obbligatorie '{'Username'}' mancanti per l'istanza del tipo di entità 'User' con il valore della chiave '{Id: 1}'.

Questa convalida può essere disabilitata, se necessario. Ad esempio:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
        .UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}

Informazioni sull'origine dei comandi per la diagnostica e gli intercettori

Problema di GitHub: #23719. Questa funzionalità è stata fornita da @Giorgi. Grazie mille!

L'oggetto CommandEventData fornito alle origini di diagnostica e agli intercettori contiene ora un valore di enumerazione che indica quale parte di ENTITY è stata responsabile della creazione del comando. Può essere usato come filtro nella diagnostica o nell'intercettore. Ad esempio, potrebbe essere necessario un intercettore che si applica solo ai comandi provenienti da SaveChanges:

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

In questo modo l'intercettore viene filtrato solo SaveChanges per gli eventi usati in un'applicazione che genera anche migrazioni e query. Ad esempio:

Saving changes for CustomersContext:

SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Gestione dei valori temporanei migliore

Problema di GitHub: #24245.

EF Core non espone valori temporanei nelle istanze del tipo di entità. Si consideri ad esempio un Blog tipo di entità con una chiave generata dall'archivio:

public class Blog
{
    public int Id { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

La Id proprietà chiave otterrà un valore temporaneo non appena un oggetto Blog viene rilevato dal contesto. Ad esempio, quando si chiama DbContext.Add:

var blog = new Blog();
context.Add(blog);

Il valore temporaneo può essere ottenuto dallo strumento di rilevamento modifiche del contesto, ma non è impostato nell'istanza dell'entità. Ad esempio, questo codice:

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

Genera l'output seguente:

Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647

Ciò è valido perché impedisce la perdita di valore temporaneo nel codice dell'applicazione in cui può essere considerato accidentalmente non temporaneo. Tuttavia, a volte è utile gestire direttamente i valori temporanei. Ad esempio, un'applicazione può voler generare i propri valori temporanei per un grafico di entità prima che vengano rilevati in modo che possano essere usati per formare relazioni usando chiavi esterne. Questa operazione può essere eseguita contrassegnando in modo esplicito i valori come temporanei. Ad esempio:

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

In EF Core 6.0 il valore rimarrà nell'istanza dell'entità anche se ora è contrassegnato come temporaneo. Ad esempio, il codice precedente genera l'output seguente:

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

Analogamente, i valori temporanei generati da EF Core possono essere impostati in modo esplicito su istanze di entità e contrassegnati come valori temporanei. Può essere usato per impostare in modo esplicito le relazioni tra le nuove entità usando i relativi valori di chiave temporanei. Ad esempio:

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

Risultato:

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

EF Core annotato per i tipi di riferimento nullable C#

Problema di GitHub: #19007.

La codebase di EF Core ora usa tipi di riferimento nullable C# (NRT) in tutto. Ciò significa che si otterranno le indicazioni corrette del compilatore per l'utilizzo null quando si usa EF Core 6.0 dal proprio codice.

Microsoft.Data.Sqlite 6.0

Suggerimento

È possibile eseguire ed eseguire il debug in tutti gli esempi illustrati di seguito scaricando il codice di esempio da GitHub.

Pool di connessioni

Problema di GitHub: #13837.

È prassi comune mantenere aperte le connessioni di database per il minor tempo possibile. Ciò consente di evitare conflitti nella risorsa di connessione. Ecco perché librerie come EF Core aprono la connessione immediatamente prima di eseguire un'operazione di database e chiuderla di nuovo immediatamente dopo. Si consideri ad esempio questo codice EF Core:

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = context.Users.ToList();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        context.SaveChanges();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

L'output di questo codice, con la registrazione per le connessioni attivate, è:

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

Si noti che la connessione viene aperta e chiusa rapidamente per ogni operazione.

Tuttavia, per la maggior parte dei sistemi di database, l'apertura di una connessione fisica al database è un'operazione costosa. Pertanto, la maggior parte dei provider di ADO.NET crea un pool di connessioni fisiche e le noleggia alle istanze in base alle DbConnection esigenze.

SQLite è leggermente diverso perché l'accesso al database è in genere solo l'accesso a un file. Ciò significa che l'apertura di una connessione a un database SQLite è in genere molto veloce. Tuttavia, questo non è sempre il caso. Ad esempio, l'apertura di una connessione a un database crittografato può essere molto lenta. Le connessioni SQLite vengono quindi raggruppate quando si usa Microsoft.Data.Sqlite 6.0.

Supporto di DateOnly e TimeOnly

Problema di GitHub: #24506.

Microsoft.Data.Sqlite 6.0 supporta i nuovi DateOnly tipi e TimeOnly di .NET 6. Questi possono essere usati anche in EF Core 6.0 con il provider SQLite. Come sempre con SQLite, il sistema di tipi nativo significa che i valori di questi tipi devono essere archiviati come uno dei quattro tipi supportati. Microsoft.Data.Sqlite li archivia come TEXT. Ad esempio, un'entità che usa questi tipi:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    
    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

Mappe alla tabella seguente nel database SQLite:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

I valori possono quindi essere salvati, sottoposti a query e aggiornati nel modo normale. Ad esempio, questa query LINQ di EF Core:

var users = context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToList();

Viene convertito nel codice seguente in SQLite:

SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'

E restituisce usa solo con compleanni prima del 1900 CE:

Found 'ajcvickers'
Found 'wendy'

Savepoints API

Problema di GitHub: #20228.

È stata standardizzata un'API comune per i punti di salvataggio nei provider di ADO.NET. Microsoft.Data.Sqlite supporta ora questa API, tra cui:

L'uso di un punto di salvataggio consente il rollback di parte di una transazione senza eseguire il rollback dell'intera transazione. Ad esempio, il codice seguente:

  • Crea una transazione
  • Invia un aggiornamento al database
  • Crea un punto di salvataggio
  • Invia un altro aggiornamento al database
  • Esegue il rollback al punto di salvataggio creato in precedenza
  • Esegue il commit della transazione
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
connection.Open();

using var transaction = connection.BeginTransaction();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    command.ExecuteNonQuery();
}

transaction.Save("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    command.ExecuteNonQuery();
}

transaction.Rollback("MySavepoint");

transaction.Commit();

Ciò comporterà il commit del primo aggiornamento nel database, mentre il secondo aggiornamento non viene eseguito il commit perché è stato eseguito il rollback del punto di salvataggio prima di eseguire il commit della transazione.

Timeout dei comandi nel stringa di connessione

Problema di GitHub: #22505. Questa funzionalità è stata fornita da @nmichels. Grazie mille!

ADO.NET provider supportano due timeout distinti:

  • Timeout della connessione, che determina il tempo massimo di attesa quando si effettua una connessione al database.
  • Timeout del comando, che determina il tempo massimo di attesa per il completamento dell'esecuzione di un comando.

Il timeout del comando può essere impostato dal codice usando DbCommand.CommandTimeout. Molti provider ora espongono anche questo timeout dei comandi nel stringa di connessione. Microsoft.Data.Sqlite segue questa tendenza con la Command Timeout parola chiave stringa di connessione. Ad esempio, "Command Timeout=60;DataSource=test.db" userà 60 secondi come timeout predefinito per i comandi creati dalla connessione.

Suggerimento

Sqlite considera Default Timeout come sinonimo Command Timeout di e quindi può essere usato se preferito.