Nouveautés d’EF Core 6.0

EF Core 6.0 est livré à NuGet. Cette page contient une vue d’ensemble des modifications intéressantes introduites dans cette version.

Conseil

Vous pouvez exécuter et déboguer dans les exemples indiqués ci-dessous en téléchargeant l’exemple de code à partir de GitHub.

SQL Server tables temporelles

Problème GitHub : #4693.

SQL Server tables temporelles effectuent automatiquement le suivi de toutes les données stockées dans une table, même après la mise à jour ou la suppression de ces données. Cela est réalisé en créant une « table d’historique » parallèle dans laquelle les données historiques horodatées sont stockées chaque fois qu’une modification est apportée à la table principale. Cela permet aux données historiques d’être interrogées, telles que pour l’audit ou restaurées, telles que pour la récupération après la mutation accidentelle ou la suppression.

EF Core prend désormais en charge les éléments suivants :

  • Création de tables temporelles à l’aide de migrations
  • Transformation des tables existantes en tables temporelles, à nouveau à l’aide de migrations
  • Interrogation des données historiques
  • Restauration des données à partir d’un point donné dans le passé

Configuration d’une table temporelle

Le générateur de modèles peut être utilisé pour configurer une table comme temporelle. Par exemple :

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

Lorsque vous utilisez EF Core pour créer la base de données, la nouvelle table est configurée en tant que table temporelle avec les SQL Server valeurs par défaut pour les horodatages et la table d’historique. Par exemple, considérez un type d’entité Employee :

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 table temporelle créée ressemble à ceci :

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

Notez que SQL Server crée deux colonnes masquées appelées datetime2PeriodEnd et PeriodStart. Ces « colonnes de période » représentent l’intervalle de temps pendant lequel les données de la ligne existaient. Ces colonnes sont mappées aux propriétés d’ombre dans le modèle EF Core, ce qui leur permet d’être utilisées dans les requêtes comme indiqué plus loin.

Important

Les heures de ces colonnes sont toujours l’heure UTC générée par SQL Server. Les heures UTC sont utilisées pour toutes les opérations impliquant des tables temporelles, comme dans les requêtes indiquées ci-dessous.

Notez également qu’une table d’historique associée appelée EmployeeHistory est créée automatiquement. Les noms des colonnes de période et de la table d’historique peuvent être modifiés avec une configuration supplémentaire au générateur de modèles. Par exemple :

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

Ceci est répercuté dans la table créée par 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]))');

Utilisation de tables temporelles

La plupart du temps, les tables temporelles sont utilisées comme n’importe quelle autre table. Autrement dit, les colonnes de période et les données historiques sont gérées de manière transparente par SQL Server afin que l’application puisse les ignorer. Par exemple, de nouvelles entités peuvent être enregistrées dans la base de données de la manière 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();

Ces données peuvent ensuite être interrogées, mises à jour et supprimées de la manière normale. Par exemple :

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

En outre, après une requête de suivi normale, les valeurs des colonnes de période des données actuelles sont accessibles à partir des entités suivies. Par exemple :

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

Cela imprime :

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

Notez que la ValidTo colonne (par défaut appelée PeriodEnd) contient la datetime2 valeur maximale. Il s’agit toujours du cas pour les lignes actuelles du tableau. Les ValidFrom colonnes (par défaut appelées PeriodStart) contiennent l’heure UTC que la ligne a été insérée.

Interrogation des données historiques

EF Core prend en charge les requêtes qui incluent des données historiques via plusieurs nouveaux opérateurs de requête :

  • TemporalAsOf: retourne des lignes actives (actuelles) à l’heure UTC donnée. Il s’agit d’une seule ligne de la table ou de la table d’historique actuelle pour une clé primaire donnée.
  • TemporalAll: retourne toutes les lignes des données historiques. Il s’agit généralement de nombreuses lignes de la table d’historique et/ou de la table actuelle pour une clé primaire donnée.
  • TemporalFromTo: retourne toutes les lignes actives entre deux heures UTC données. Il peut s’agir de nombreuses lignes de la table d’historique et/ou de la table actuelle d’une clé primaire donnée.
  • TemporalBetween: identique TemporalFromToà , sauf que les lignes sont incluses qui sont devenues actives sur la limite supérieure.
  • TemporalContainedIn: retourne toutes les lignes qui ont commencé à être actives et ont terminé d’être actives entre deux heures UTC données. Il peut s’agir de nombreuses lignes de la table d’historique et/ou de la table actuelle d’une clé primaire donnée.

Notes

Consultez la documentation sur les tables temporelles SQL Server pour plus d’informations sur les lignes qui sont incluses pour chacun de ces opérateurs.

Par exemple, après avoir effectué des mises à jour et des suppressions sur nos données, nous pouvons exécuter une requête à l’aide TemporalAll de la vue des données historiques :

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

Notez comment EF. La méthode de propriété peut être utilisée pour accéder aux valeurs des colonnes de période. Ceci est utilisé dans la OrderBy clause pour trier les données, puis dans une projection pour inclure ces valeurs dans les données retournées.

Cette requête renvoie les données suivantes :

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

Notez que la dernière ligne retournée a cessé d’être active à 8/26/2021 44:59 PM. Cela est dû au fait que la ligne de Rainbow Dash a été supprimée de la table principale à ce moment-là. Nous verrons plus tard comment ces données peuvent être restaurées.

Des requêtes similaires peuvent être écrites à l’aide TemporalFromTode , TemporalBetweenou TemporalContainedIn. Par exemple :

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

Cette requête retourne les lignes suivantes :

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

Restauration des données historiques

Comme mentionné ci-dessus, Rainbow Dash a été supprimé de la Employees table. C’était clairement une erreur, alors revenons à un point dans le temps et restaurez la ligne manquante à partir de cette époque.

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

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

Cette requête retourne une seule ligne pour Rainbow Dash, car elle était à l’heure UTC donnée. Toutes les requêtes utilisant des opérateurs temporels ne sont pas suivis par défaut. Par conséquent, l’entité retournée ici n’est pas suivie. Cela est logique, car il n’existe pas actuellement dans la table principale. Pour réinscrire l’entité dans la table principale, nous l’appelons simplement comme Added et ensuite SaveChanges.

Après avoir réinscrit la ligne Rainbow Dash, l’interrogation des données historiques montre que la ligne a été restaurée comme elle était au moment UTC donné :

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

Migration Bundles

Problème GitHub : #19693.

Les migrations EF Core sont utilisées pour générer des mises à jour de schéma de base de données en fonction des modifications apportées au modèle EF. Ces mises à jour de schéma doivent être appliquées au moment du déploiement de l’application, souvent dans le cadre d’un système d’intégration continue/de déploiement continu (C.I./C.D.).

EF Core inclut désormais une nouvelle façon d’appliquer ces mises à jour de schéma : bundles de migration. Un bundle de migration est un petit exécutable contenant des migrations et le code nécessaire pour appliquer ces migrations à la base de données.

Notes

Consultez Présentation des bundles de migration EF Core compatibles avec DevOps sur le blog .NET pour une discussion plus approfondie sur les migrations, les bundles et le déploiement.

Les bundles de migration sont créés à l’aide de l’outil dotnet ef en ligne de commande. Vérifiez que vous avez installé la dernière version de l’outil avant de continuer.

Un bundle a besoin de migrations à inclure. Celles-ci sont créées à l’aide dotnet ef migrations add de la documentation sur les migrations. Une fois que vous avez des migrations prêtes à être déployées, créez un bundle à l’aide du dotnet ef migrations bundlefichier . Par exemple :

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>

La sortie est un exécutable adapté à votre système d’exploitation cible. Dans mon cas, il s’agit de Windows x64, donc j’obtiens une efbundle.exe suppression dans mon dossier local. L’exécution de cet exécutable applique les migrations contenues dans celle-ci :

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

Les migrations sont appliquées à la base de données uniquement si elles n’ont pas déjà été appliquées. Par exemple, l’exécution du même bundle ne fait rien, car il n’existe aucune nouvelle migration à appliquer :

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

Toutefois, si des modifications sont apportées au modèle et que d’autres migrations sont générées avec dotnet ef migrations add, elles peuvent être regroupées dans un nouveau fichier exécutable prêt à s’appliquer. Par exemple :

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>

Notez que l’option --force peut être utilisée pour remplacer le bundle existant avec un nouveau.

L’exécution de ce nouveau bundle applique ces deux nouvelles migrations à la base de données :

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

Par défaut, le bundle utilise la chaîne de connexion de base de données à partir de la configuration de votre application. Toutefois, une autre base de données peut être migrée en passant la chaîne de connexion sur la ligne de commande. Par exemple :

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>

Notez que cette fois, les trois migrations ont été appliquées, car aucune d’entre elles n’avait encore été appliquée à la base de données de production.

D’autres options peuvent être passées à la ligne de commande. Voici quelques options courantes :

  • --output pour spécifier le chemin d’accès du fichier exécutable à créer.
  • --context pour spécifier le type DbContext à utiliser lorsque le projet contient plusieurs types de contexte.
  • --project pour spécifier le projet à utiliser. La valeur par défaut est le répertoire de travail actif.
  • --startup-project pour spécifier le projet de démarrage à utiliser. La valeur par défaut est le répertoire de travail actif.
  • --no-build pour empêcher la génération du projet avant d’exécuter la commande. Cela ne doit être utilisé que si le projet est connu pour être à jour.
  • --verbose pour afficher des informations détaillées sur ce que fait la commande. Utilisez cette option lorsque vous incluez des informations dans les rapports de bogues.

Permet dotnet ef migrations bundle --help d’afficher toutes les options disponibles.

Notez que chaque migration est appliquée par défaut dans sa propre transaction. Consultez le problème GitHub #22616 pour une discussion sur les améliorations futures possibles dans ce domaine.

Configuration du modèle de pré-convention

Problème GitHub : #12229.

Les versions précédentes d’EF Core nécessitent que le mappage pour chaque propriété d’un type donné soit configuré explicitement lorsque ce mappage diffère de la valeur par défaut. Cela inclut des « facettes » comme la longueur maximale des chaînes et la précision décimale, ainsi que la conversion de valeur pour le type de propriété.

Cela nécessite soit :

  • Configuration du générateur de modèles pour chaque propriété
  • Attribut de mappage sur chaque propriété
  • Itération explicite sur toutes les propriétés de tous les types d’entités et utilisation des API de métadonnées de bas niveau lors de la génération du modèle.

Notez que l’itération explicite est sujette aux erreurs et difficile à effectuer de manière robuste, car la liste des types d’entités et des propriétés mappées peut ne pas être finale au moment où cette itération se produit.

EF Core 6.0 permet de spécifier cette configuration de mappage une fois pour un type donné. Elle sera ensuite appliquée à toutes les propriétés de ce type dans le modèle. Il s’agit de la « configuration du modèle de pré-convention », car elle configure les aspects du modèle qui sont ensuite utilisés par les conventions de création de modèle. Cette configuration est appliquée en remplaçant ConfigureConventions votre DbContext:

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

Par exemple, considérez les types d’entités suivants :

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

Toutes les propriétés de chaîne peuvent être configurées pour être ANSI (au lieu d’Unicode) et avoir une longueur maximale de 1 024 :

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

Toutes les propriétés DateTime peuvent être converties en entiers 64 bits dans la base de données, en utilisant la conversion par défaut de DateTimes en entiers longs :

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

Toutes les propriétés bool peuvent être converties en entiers 0 ou 1 à l’aide de l’un des convertisseurs de valeurs intégrés :

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

En supposant qu’il s’agit Session d’une propriété temporaire de l’entité et qu’elle ne doit pas être conservée, elle peut être ignorée partout dans le modèle :

configurationBuilder
    .IgnoreAny<Session>();

La configuration du modèle de pré-convention est très utile lors de l’utilisation d’objets valeur. Par exemple, le type Money dans le modèle ci-dessus est représenté par un struct en lecture seule :

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
}

Il est ensuite sérialisé vers et depuis JSON à l’aide d’un convertisseur de valeurs personnalisé :

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

Ce convertisseur de valeur peut être configuré une seule fois pour toutes les utilisations de Money :

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

Notez également que des facettes supplémentaires peuvent être spécifiées pour la colonne de chaîne dans laquelle le JSON sérialisé est stocké. Dans ce cas, la colonne est limitée à une longueur maximale de 64.

Les tables créées pour SQL Server à l’aide de migrations montrent comment la configuration a été appliquée à toutes les colonnes mappées :

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

Il est également possible de spécifier un mappage de type par défaut pour un type donné. Par exemple :

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

Cela est rarement nécessaire, mais peut être utile si un type est utilisé dans une requête d’une manière qui n’est pas corrélée à n’importe quelle propriété mappée du modèle.

Notes

Consultez l’annonce d’Entity Framework Core 6.0 Preview 6 : Configurer des conventions sur le blog .NET pour plus de discussions et d’exemples de configuration de modèle de pré-convention.

Modèles compilés

Problème GitHub : #1906.

Les modèles compilés peuvent améliorer le temps de démarrage d’EF Core pour les applications avec de grands modèles. Un modèle volumineux signifie généralement 100 à 1 000 de types d’entités et de relations.

L’heure de démarrage signifie l’heure d’exécution de la première opération sur un DbContext lorsque ce type DbContext est utilisé pour la première fois dans l’application. Notez que la création d’une instance DbContext n’entraîne pas l’initialisation du modèle EF. Au lieu de cela, les premières opérations classiques qui entraînent l’initialisation du modèle incluent l’appel DbContext.Add ou l’exécution de la première requête.

Les modèles compilés sont créés à l’aide de l’outil dotnet ef en ligne de commande. Vérifiez que vous avez installé la dernière version de l’outil avant de continuer.

Une nouvelle dbcontext optimize commande est utilisée pour générer le modèle compilé. Par exemple :

dotnet ef dbcontext optimize

Les options et --namespace les --output-dir options peuvent être utilisées pour spécifier le répertoire et l’espace de noms dans lesquels le modèle compilé sera généré. Par exemple :

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>

La sortie de l’exécution de cette commande inclut un élément de code permettant de copier-coller dans votre configuration DbContext afin qu’EF Core utilise le modèle compilé. Par exemple :

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

Démarrage du modèle compilé

Il n’est généralement pas nécessaire d’examiner le code de démarrage généré. Toutefois, il peut parfois être utile de personnaliser le modèle ou son chargement. Le code d’amorçage ressemble à ceci :

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

Il s’agit d’une classe partielle avec des méthodes partielles qui peuvent être implémentées pour personnaliser le modèle en fonction des besoins.

De plus, plusieurs modèles compilés peuvent être générés pour les types DbContext qui peuvent utiliser différents modèles en fonction de la configuration du runtime. Ceux-ci doivent être placés dans différents dossiers et espaces de noms, comme indiqué ci-dessus. Les informations d’exécution, telles que la chaîne de connexion, peuvent ensuite être examinées et le modèle correct retourné en fonction des besoins. Par exemple :

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

Limites

Les modèles compilés présentent certaines limitations :

En raison de ces limitations, vous ne devez utiliser que des modèles compilés si votre temps de démarrage EF Core est trop lent. La compilation de petits modèles n’en vaut généralement pas la peine.

Si vous prenez en charge l’une de ces fonctionnalités est essentielle à votre réussite, votez pour les problèmes appropriés liés ci-dessus.

Benchmarks

Conseil

Vous pouvez essayer de compiler un modèle volumineux et d’exécuter un benchmark sur celui-ci en téléchargeant l’exemple de code à partir de GitHub.

Le modèle dans le référentiel GitHub référencé ci-dessus contient 449 types d’entités, 6390 propriétés et 720 relations. Il s’agit d’un modèle modérément volumineux. À l’aide de BenchmarkDotNet pour mesurer, la durée moyenne de la première requête est de 1,02 seconde sur un ordinateur portable raisonnablement puissant. L’utilisation de modèles compilés fait passer cette valeur à 117 millisecondes sur le même matériel. Une amélioration de 8x à 10x comme celle-ci reste relativement constante à mesure que la taille du modèle augmente.

Amélioration des performances du modèle compilé

Notes

Consultez l’annonce d’Entity Framework Core 6.0 Preview 5 : Modèles compilés sur le blog .NET pour une présentation plus approfondie des performances de démarrage d’EF Core et des modèles compilés.

Amélioration des performances sur TechEmpower Fortunes

Problème GitHub : #23611.

Nous avons apporté des améliorations significatives aux performances des requêtes pour EF Core 6.0. Plus précisément :

  • Les performances d’EF Core 6.0 sont désormais de 70 % plus rapides sur le benchmark TechEmpower Fortunes standard, comparativement à 5,0.
    • Il s’agit de l’amélioration complète des performances de pile, y compris des améliorations dans le code d’évaluation, le runtime .NET, etc.
  • EF Core 6.0 lui-même est de 31 % plus rapide en exécutant des requêtes non suivies.
  • Les allocations de tas ont été réduites de 43 % lors de l’exécution de requêtes.

Après ces améliorations, l’écart entre le populaire « micro-ORM » Dapper et EF Core dans le benchmark TechEmpower Fortunes a réduit de 55 % à un peu moins de 5 %.

Notes

Consultez l’annonce d’Entity Framework Core 6.0 Preview 4 : Performance Edition sur le blog .NET pour une présentation détaillée des améliorations des performances des requêtes dans EF Core 6.0.

Améliorations apportées au fournisseur Cosmos

EF Core 6.0 contient de nombreuses améliorations apportées au fournisseur de base de données Azure Cosmos DB.

Conseil

Vous pouvez exécuter et déboguer dans tous les exemples spécifiques à Cosmos en téléchargeant l’exemple de code à partir de GitHub.

Propriété implicite par défaut

Problème GitHub : #24803.

Lors de la génération d’un modèle pour le fournisseur Cosmos, EF Core 6.0 marque les types d’entités enfants comme appartenant à leur entité parente par défaut. Cela supprime la nécessité d’une grande partie des appels et OwnsOne appels OwnsMany dans le modèle Cosmos. Cela facilite l’incorporation de types enfants dans le document pour le type parent, qui est généralement le moyen approprié de modéliser les parents et les enfants dans une base de données de documents.

Par exemple, considérez ces types d’entités :

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

Dans EF Core 5.0, ces types auraient été modélisés pour Cosmos avec la configuration suivante :

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

Dans EF Core 6.0, la propriété est implicite, ce qui réduit la configuration du modèle à :

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

Les documents Cosmos obtenus ont les parents, les enfants, les animaux de compagnie et l’adresse de la famille incorporés dans le document de famille. Par exemple :

{
  "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
}

Notes

Il est important de se rappeler que la OwnsOne/OwnsMany configuration doit être utilisée si vous devez configurer davantage ces types détenus.

Collection de types primitifs

Problème GitHub : #14762.

EF Core 6.0 mappe en mode natif des collections de types primitifs lors de l’utilisation du fournisseur de base de données Cosmos. Par exemple, prenons le type d'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; }
}

La liste et le dictionnaire peuvent être renseignés et insérés dans la base de données de la manière 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();

Cela entraîne le document JSON suivant :

{
    "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
}

Ces collections peuvent ensuite être mises à jour, de nouveau de la manière normale :

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

context.SaveChanges();

Limites :

  • Seuls les dictionnaires avec des clés de chaîne sont pris en charge
  • L’interrogation dans le contenu des collections primitives n’est actuellement pas prise en charge. Votez pour #16926, #25700 et #25701 si ces fonctionnalités sont importantes pour vous.

Traductions vers des fonctions intégrées

Problème GitHub : #16143.

Le fournisseur Cosmos convertit désormais davantage de méthodes de bibliothèque de classes de base (BCL) en fonctions intégrées Cosmos. Les tableaux suivants montrent les traductions nouvelles dans EF Core 6.0.

Traductions de chaînes

Méthode BCL Fonction intégrée Notes
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
Opérateur : + CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL Seuls les appels ne respectant pas la casse

Les traductions pour LOWER, , LTRIM, RTRIMTRIMet SUBSTRINGUPPERont été contribuées par @Marusyk. Merci beaucoup !

Par exemple :

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

Ce qui se traduit par :

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

Traductions mathématiques

Méthode BCL Fonction intégrée
Math.Abs ou MathF.Abs ABS
Math.Acos ou MathF.Acos ACOS
Math.Asin ou MathF.Asin ASIN
Math.Atan ou MathF.Atan ATAN
Math.Atan2 ou MathF.Atan2 ATN2
Math.Ceiling ou MathF.Ceiling CEILING
Math.Cos ou MathF.Cos COS
Math.Exp ou MathF.Exp EXP
Math.Floor ou MathF.Floor FLOOR
Math.Log ou MathF.Log LOG
Math.Log10 ou MathF.Log10 LOG10
Math.Pow ou MathF.Pow POWER
Math.Round ou MathF.Round ROUND
Math.Sign ou MathF.Sign SIGN
Math.Sin ou MathF.Sin SIN
Math.Sqrt ou MathF.Sqrt SQRT
Math.Tan ou MathF.Tan TAN
Math.Truncate ou MathF.Truncate TRUNC
DbFunctions.Random RAND

Ces traductions ont été contribuées par @Marusyk. Merci beaucoup !

Par exemple :

var hypotenuse = 42.42;
var mathResults
    = 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))
        .ToList();

Ce qui se traduit par :

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

Traductions DateTime

Méthode BCL Fonction intégrée
DateTime.UtcNow GetCurrentDateTime

Ces traductions ont été contribuées par @Marusyk. Merci beaucoup !

Par exemple :

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

Ce qui se traduit par :

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

Requêtes SQL brutes avec FromSql

Problème GitHub : #17311.

Parfois, il est nécessaire d’exécuter une requête SQL brute au lieu d’utiliser LINQ. Cela est désormais pris en charge avec le fournisseur Cosmos à l’aide de la FromSql méthode. Cela fonctionne de la même façon qu’avec les fournisseurs relationnels. Par exemple :

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

Qui est exécuté comme suit :

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

Requêtes distinctes

Problème GitHub : #16144.

Les requêtes simples utilisant Distinct sont désormais traduites. Par exemple :

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

Ce qui se traduit par :

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

Diagnostics

Problème GitHub : #17298.

Le fournisseur Cosmos enregistre désormais plus d’informations de diagnostic, notamment les événements d’insertion, d’interrogation, de mise à jour et de suppression de données de la base de données. Les unités de requête (RU) sont incluses dans ces événements chaque fois que cela est approprié.

Notes

Les journaux d’activité s’affichent ici EnableSensitiveDataLogging() afin que les valeurs d’ID soient affichées.

L’insertion d’un élément dans la base de données Cosmos génère l’événement CosmosEventId.ExecutedCreateItem . Par exemple, ce code :

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

Enregistre l’événement de diagnostic suivant :

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'

La récupération d’éléments à partir de la base de données Cosmos à l’aide d’une requête génère l’événement CosmosEventId.ExecutingSqlQuery , puis un ou plusieurs CosmosEventId.ExecutedReadNext événements pour les éléments lus. Par exemple, ce code :

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

Enregistre les événements de diagnostic suivants :

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

La récupération d’un élément unique à partir de la base de données Cosmos à l’aide Find d’une clé de partition génère les événements et CosmosEventId.ExecutedReadItem les CosmosEventId.ExecutingReadItem événements. Par exemple, ce code :

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

Enregistre les événements de diagnostic suivants :

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'

L’enregistrement d’un élément mis à jour dans la base de données Cosmos génère l’événement CosmosEventId.ExecutedReplaceItem . Par exemple, ce code :

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

Enregistre l’événement de diagnostic suivant :

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'

La suppression d’un élément de la base de données Cosmos génère l’événement CosmosEventId.ExecutedDeleteItem . Par exemple, ce code :

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

Enregistre l’événement de diagnostic suivant :

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'

Configurer le débit

Problème GitHub : #17301.

Le modèle Cosmos peut désormais être configuré avec un débit manuel ou à mise à l’échelle automatique. Ces valeurs approvisionnent le débit sur la base de données. Par exemple :

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

En outre, les types d’entités individuels peuvent être configurés pour approvisionner le débit pour le conteneur correspondant. Par exemple :

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

Configurer la durée de vie

Problème GitHub : #17307.

Les types d’entités dans le modèle Cosmos peuvent désormais être configurés avec la durée de vie par défaut et la durée de vie du magasin analytique. Par exemple :

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

Résoudre la fabrique de client HTTP

Problème GitHub : #21274. Cette fonctionnalité a été apportée par @dnperfors. Merci beaucoup !

L’utilisation HttpClientFactory par le fournisseur Cosmos peut désormais être définie explicitement. Cela peut être particulièrement utile lors du test, par exemple pour contourner la validation des certificats lors de l’utilisation de l’émulateur Cosmos sur Linux :

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

Notes

Consultez Prise du fournisseur EF Core Azure Cosmos DB pour une version d’évaluation sur le blog .NET pour obtenir un exemple détaillé d’application des améliorations du fournisseur Cosmos à une application existante.

Améliorations apportées à la génération automatique à partir d’une base de données existante

EF Core 6.0 contient plusieurs améliorations lors de l’ingénierie inverse d’un modèle EF à partir d’une base de données existante.

Génération automatique de relations plusieurs-à-plusieurs

Problème GitHub : #22475.

EF Core 6.0 détecte les tables de jointure simples et génère automatiquement un mappage plusieurs-à-plusieurs pour eux. Par exemple, considérez les tables pour Posts et Tagsune table PostTag de jointure qui les connecte :

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

Ces tables peuvent être générées automatiquement à partir de la ligne de commande. Par exemple :

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

Cela entraîne une classe pour 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; }
}

Et une classe pour 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; }
}

Mais aucune classe pour la PostTag table. Au lieu de cela, la configuration d’une relation plusieurs-à-plusieurs est générée automatiquement :

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

Types référence nullableS C# de structure automatique

Problème GitHub : #15520.

EF Core 6.0 génère désormais une génération automatique d’un modèle EF et des types d’entités qui utilisent des types référence nullable C# (NRT). L’utilisation de la NRT est automatiquement générée lorsque la prise en charge de NRT est activée dans le projet C# dans lequel le code est généré automatiquement.

Par exemple, le tableau suivant Tags contient les deux colonnes de chaîne non nullables :

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

Cela entraîne des propriétés de chaîne nullables et non nullables correspondantes dans la classe générée :

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

De même, les tableaux suivants Posts contiennent une relation requise avec la Blogs table :

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

Cela entraîne la génération automatique d’une relation non nullable (obligatoire) entre les blogs :

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

Et billets :

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

Enfin, les propriétés DbSet dans le DbContext généré sont créées de manière conviviale NRT. Par exemple :

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

Les commentaires de base de données sont générés automatiquement en commentaires de code

Problème GitHub : #19113. Cette fonctionnalité a été apportée par @ErikEJ. Merci beaucoup !

Les commentaires sur les tables et colonnes SQL sont désormais générés automatiquement dans les types d’entités créés lors de l’ingénierie inverse d’un modèle EF Core à partir d’une base de données SQL Server existante.

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

Améliorations apportées aux requêtes LINQ

EF Core 6.0 contient plusieurs améliorations dans la traduction et l’exécution des requêtes LINQ.

Amélioration de la prise en charge de GroupBy

Problèmes GitHub : #12088, #13805 et #22609.

EF Core 6.0 contient une meilleure prise en charge des GroupBy requêtes. Plus précisément, EF Core maintenant :

  • Traduire GroupBy suivi d’un FirstOrDefault groupe (ou similaire)
  • Prend en charge la sélection des résultats N principaux d’un groupe
  • Développe les navigations après l’application de l’opérateur GroupBy

Voici des exemples de requêtes provenant de rapports clients et de leur traduction sur SQL Server.

Exemple 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]

Exemple 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]

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

Exemple 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]

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

Exemple 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]))

Exemple 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]

Exemple 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]

Exemple 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]

Exemple 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]

Exemple 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]

Exemple 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]

Exemple 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]

Modèle

Les types d’entités utilisés pour ces exemples sont les suivants :

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

Traduire String.Concat avec plusieurs arguments

Problème GitHub : #23859. Cette fonctionnalité a été apportée par @wmeints. Merci beaucoup !

À compter d’EF Core 6.0, les appels vers String.Concat plusieurs arguments sont désormais traduits en SQL. Par exemple, la requête suivante :

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

Sera traduit en SQL suivant lors de l’utilisation de 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

Intégration plus fluide à System.Linq.Async

Problème GitHub : #24041.

Le package System.Linq.Async ajoute le traitement LINQ asynchrone côté client. L’utilisation de ce package avec les versions précédentes d’EF Core était fastidieuse en raison d’un conflit d’espaces de noms pour les méthodes LINQ asynchrones. Dans EF Core 6.0, nous avons profité de la correspondance de modèle C# pour IAsyncEnumerable<T> que l’EF Core DbSet<TEntity> exposé n’ait pas besoin d’implémenter l’interface directement.

Notez que la plupart des applications n’ont pas besoin d’utiliser System.Linq.Async, car les requêtes EF Core sont généralement entièrement traduites sur le serveur.

Problème GitHub : #23921.

Dans EF Core 6.0, nous avons assoupli les exigences de paramètre pour FreeText(DbFunctions, String, String) et Contains. Cela permet à ces fonctions d’être utilisées avec des colonnes binaires ou avec des colonnes mappées à l’aide d’un convertisseur de valeurs. Par exemple, considérez un type d’entité avec une Name propriété définie comme un objet valeur :

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

Il s’agit d’un mappage JSON dans la base de données :

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

Une requête peut désormais être exécutée à l’aide Contains ou FreeText même si le type de la propriété n’est Name pas string. Par exemple :

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

Cela génère le code SQL suivant lors de l’utilisation de SQL Server :

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

Traduire ToString sur SQLite

Problème GitHub : #17223. Cette fonctionnalité a été apportée par @ralmsdeveloper. Merci beaucoup !

Les appels à effectuer ToString() sont désormais traduits en SQL lors de l’utilisation du fournisseur de base de données SQLite. Cela peut être utile pour les recherches de texte impliquant des colonnes non-chaînes. Par exemple, considérez un type d’entité User qui stocke les numéros de téléphone sous forme de valeurs numériques :

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

ToString peut être utilisé pour convertir le nombre en chaîne dans la base de données. Nous pouvons ensuite utiliser cette chaîne avec une fonction telle que LIKE pour rechercher des nombres qui correspondent à un modèle. Par exemple, pour rechercher tous les nombres contenant 555 :

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

Cela se traduit par le code SQL suivant lors de l’utilisation d’une base de données SQLite :

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

Notez que la traduction de ToString() SQL Server est déjà prise en charge dans EF Core 5.0 et peut également être prise en charge par d’autres fournisseurs de base de données.

Ef. Functions.Random

Problème GitHub : #16141. Cette fonctionnalité a été apportée par @RaymondHuy. Merci beaucoup !

EF.Functions.Random correspond à une fonction de base de données qui retourne un nombre pseudo-aléatoire compris entre 0 et 1 exclusif. Les traductions ont été implémentées dans le dépôt EF Core pour SQL Server, SQLite et Cosmos. Par exemple, considérez un type d’entité User avec une Popularity propriété :

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

Popularity peut avoir des valeurs comprises entre 1 et 5 inclus. À l’aide EF.Functions.Random de nous pouvons écrire une requête pour renvoyer tous les utilisateurs avec une popularité choisie de manière aléatoire :

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

Cela se traduit par le code SQL suivant lors de l’utilisation d’une base de données SQL Server :

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

Amélioration de la traduction SQL Server pour IsNullOrWhitespace

Problème GitHub : #22916. Cette fonctionnalité a été apportée par @Marusyk. Merci beaucoup !

Considérez la requête suivante :

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

Avant EF Core 6.0, ceci a été traduit dans les éléments suivants sur 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''))

Cette traduction a été améliorée pour EF Core 6.0 vers :

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

Définition d’une requête pour un fournisseur en mémoire

Problème GitHub : #24600.

Une nouvelle méthode ToInMemoryQuery peut être utilisée pour écrire une requête de définition sur la base de données en mémoire pour un type d’entité donné. Cela est le plus utile pour créer l’équivalent des vues sur la base de données en mémoire, en particulier lorsque ces vues retournent des types d’entités sans clé. Par exemple, envisagez une base de données client pour les clients basés au Royaume-Uni. Chaque client a une adresse :

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

Imaginez maintenant que nous voulons une vue sur ces données qui indiquent le nombre de clients présents dans chaque zone de code post-code. Nous pouvons créer un type d’entité sans clé pour représenter ceci :

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

Définissez une propriété DbSet pour celle-ci sur DbContext, ainsi que des ensembles pour d’autres types d’entités de niveau supérieur :

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

Ensuite, dans OnModelCreating, nous pouvons écrire une requête LINQ qui définit les données à retourner pour 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()
                        }));
}

Cela peut ensuite être interrogé comme n’importe quelle autre propriété DbSet :

var results = context.CustomerDensities.ToList();

Traduire la sous-chaîne avec un seul paramètre

Problème GitHub : #20173. Cette fonctionnalité a été apportée par @stevendarby. Merci beaucoup !

EF Core 6.0 traduit désormais les utilisations d’un string.Substring seul argument. Par exemple :

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

Cela se traduit par le code SQL suivant lors de l’utilisation de 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'

Requêtes fractionnées pour les collections de non-navigation

Problème GitHub : #21234.

EF Core prend en charge le fractionnement d’une requête LINQ unique en plusieurs requêtes SQL. Dans EF Core 6.0, cette prise en charge a été développée pour inclure des cas où les collections de non-navigation sont contenues dans la projection de requête.

Voici des exemples de requêtes montrant la traduction sur SQL Server dans une seule requête ou plusieurs requêtes.

Exemple 1 :

Requête LINQ :

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

Requête SQL unique :

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]

Plusieurs requêtes 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]

Exemple 2 :

Requête LINQ :

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

Requête SQL unique :

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]

Plusieurs requêtes 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]

Exemple 3 :

Requête LINQ :

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

Requête SQL unique :

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]

Plusieurs requêtes 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]

Supprimer la dernière clause ORDER BY lors de la jointure pour la collection

Problème GitHub : #19828.

Lors du chargement d’entités un à plusieurs associées, EF Core ajoute des clauses ORDER BY pour vérifier que toutes les entités associées pour une entité donnée sont regroupées. Toutefois, la dernière clause ORDER BY n’est pas nécessaire pour EF générer les regroupements nécessaires et peut avoir un impact sur les performances. Par conséquent, EF Core 6.0 cette clause est supprimée.

Par exemple, envisagez la requête suivante :

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

Avec EF Core 5.0 sur SQL Server, cette requête est traduite en :

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]

Avec EF Core 6.0, il est plutôt traduit en :

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]

Baliser des requêtes avec le nom de fichier et le numéro de ligne

Problème GitHub : #14176. Cette fonctionnalité a été apportée par @michalczerwinski. Merci beaucoup !

Les balises de requête permettent d’ajouter une balise texturale à une requête LINQ de sorte qu’elle soit ensuite incluse dans le SQL généré. Dans EF Core 6.0, il peut être utilisé pour baliser des requêtes avec le nom de fichier et le numéro de ligne du code LINQ. Par exemple :

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

Cela entraîne la génération de SQL suivante lors de l’utilisation de 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%')

Modifications apportées à la gestion facultative dépendante de propriété

Problème GitHub : #24558.

Il devient difficile de savoir si une entité dépendante facultative existe ou non lorsqu’elle partage une table avec son entité principale. Cela est dû au fait qu’il existe une ligne dans la table pour la personne dépendante, car le principal en a besoin, qu’il soit ou non dépendant. La façon de gérer cela sans ambiguïté est de s’assurer que le dépendant a au moins une propriété requise. Étant donné qu’une propriété requise ne peut pas être null, cela signifie que si la valeur de cette propriété est null, l’entité dépendante n’existe pas.

Par exemple, considérez une Customer classe où chaque client possède une propriété 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’adresse est facultative, ce qui signifie qu’elle est valide pour enregistrer un client sans adresse :

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

Toutefois, si un client a une adresse, cette adresse doit avoir au moins un code post-null :

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

Cela est assuré en marquant la Postcode propriété comme Required.

Maintenant, lorsque les clients sont interrogés, si la colonne Postcode est null, cela signifie que le client n’a pas d’adresse et que la Customer.Address propriété de navigation est laissée null. Par exemple, itérer via les clients et vérifier si l’adresse est 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}.");
    }
}

Génère les résultats suivants :

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

Envisagez plutôt le cas où aucune propriété hors de l’adresse n’est requise :

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

Maintenant, il est possible d’enregistrer à la fois un client sans adresse et un client avec une adresse où toutes les propriétés d’adresse sont null :

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

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

Toutefois, dans la base de données, ces deux cas sont indistinguishables, comme nous pouvons le voir en interrogeant directement les colonnes de base de données :

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

Pour cette raison, EF Core 6.0 vous avertit maintenant lors de l’enregistrement d’une dépendance facultative où toutes ses propriétés sont null. Par exemple :

avertissement : 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) L’entité de type « Address » avec les valeurs de clé primaire {CustomerId : -2147482646} est une dépendance facultative à l’aide du partage de tables. L’entité n’a aucune propriété avec une valeur non par défaut pour identifier si l’entité existe. Cela signifie que lorsque l’instance d’objet n’est interrogée, aucune instance d’objet n’est créée au lieu d’une instance avec toutes les propriétés définies sur les valeurs par défaut. Tous les dépendants imbriqués seront également perdus. N’enregistrez aucune instance avec uniquement des valeurs par défaut ou marquez la navigation entrante comme nécessaire dans le modèle.

Cela devient encore plus difficile où la dépendance facultative elle-même agit un principal pour une autre dépendance facultative, également mappée à la même table. Au lieu d’un avertissement, EF Core 6.0 interdit uniquement les cas de dépendances facultatives imbriquées. Par exemple, considérez le modèle suivant, où ContactInfo il appartient Customer et Address est retourné par 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; }
}

Maintenant, s’il ContactInfo.Phone est null, EF Core ne crée pas d’instance de Address si la relation est facultative, même si l’adresse elle-même peut avoir des données. Pour ce type de modèle, EF Core 6.0 lève l’exception suivante :

System.InvalidOperationException : le type d’entité « ContactInfo » est un dépendant facultatif à l’aide du partage de tables et contenant d’autres dépendants sans propriété non partagée requise pour identifier si l’entité existe. Si toutes les propriétés nullables contiennent une valeur Null dans la base de données, une instance d’objet ne sera pas créée dans la requête, ce qui entraîne la perte des valeurs dépendantes imbriquées. Ajoutez une propriété requise pour créer des instances avec des valeurs Null pour d’autres propriétés ou marquer la navigation entrante comme nécessaire pour toujours créer une instance.

La ligne de bas ici consiste à éviter le cas où une dépendance facultative peut contenir toutes les valeurs de propriété nullables et partage une table avec son principal. Il existe trois façons simples d’éviter ceci :

  1. Faites en sorte que l’élément dépendant soit requis. Cela signifie que l’entité dépendante aura toujours une valeur après son interrogation, même si toutes ses propriétés sont null.
  2. Assurez-vous que la propriété dépendante contient au moins une propriété requise, comme décrit ci-dessus.
  3. Enregistrez les dépendants facultatifs dans leur propre table, au lieu de partager une table avec le principal.

Un dépendant peut être requis à l’aide de l’attribut Required sur sa navigation :

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

Ou en spécifiant qu’il est requis dans OnModelCreating:

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

Les dépendants peuvent être enregistrés dans une autre table en spécifiant les tables à utiliser dans OnModelCreating:

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

Pour plus d’exemples de dépendances facultatives, consultez optionalDependentsSample dans GitHub pour obtenir d’autres exemples de dépendances facultatives, notamment des cas avec des dépendances facultatives imbriquées.

Nouveaux attributs de mappage

EF Core 6.0 contient plusieurs nouveaux attributs qui peuvent être appliqués au code pour modifier la façon dont il est mappé à la base de données.

UnicodeAttribute

Problème GitHub : #19794. Cette fonctionnalité a été apportée par @RaymondHuy. Merci beaucoup !

À compter d’EF Core 6.0, une propriété de chaîne peut désormais être mappée à une colonne non Unicode à l’aide d’un attribut de mappage sans spécifier directement le type de base de données. Par exemple, considérez un type d’entité Book avec une propriété pour le numéro de livre standard international (ISBN) sous la forme « 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; }
}

Étant donné que les ISBN ne peuvent pas contenir de caractères non unicode, l’attribut Unicode entraîne l’utilisation d’un type de chaîne non Unicode. En outre, MaxLength il est utilisé pour limiter la taille de la colonne de base de données. Par exemple, lors de l’utilisation de SQL Server, cela entraîne une colonne de base de données de varchar(22):

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

Notes

EF Core mappe les propriétés de chaîne aux colonnes Unicode par défaut. UnicodeAttribute est ignoré lorsque le système de base de données prend uniquement en charge les types Unicode.

PrecisionAttribute

Problème GitHub : #17914. Cette fonctionnalité a été apportée par @RaymondHuy. Merci beaucoup !

La précision et l’échelle d’une colonne de base de données peuvent désormais être configurées à l’aide d’attributs de mappage sans spécifier directement le type de base de données. Par exemple, considérez un type d’entité Product avec une propriété décimale Price :

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

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

EF Core mappe cette propriété à une colonne de base de données avec précision 10 et mise à l’échelle 2. Par exemple, sur SQL Server :

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

EntityTypeConfigurationAttribute

Problème GitHub : #23163. Cette fonctionnalité a été apportée par @KaloyanIT. Merci beaucoup !

IEntityTypeConfiguration<TEntity> les instances autorisent ModelBuilder la configuration de chaque type d’entité à contenir dans sa propre classe de configuration. Par exemple :

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

Normalement, cette classe de configuration doit être instanciée et appelée à partir de DbContext.OnModelCreating. Par exemple :

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

À compter d’EF Core 6.0, un EntityTypeConfigurationAttribute élément peut être placé sur le type d’entité afin qu’EF Core puisse rechercher et utiliser la configuration appropriée. Par exemple :

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

Cet attribut signifie qu’EF Core utilisera l’implémentation spécifiée IEntityTypeConfiguration chaque fois que le Book type d’entité est inclus dans un modèle. Le type d’entité est inclus dans un modèle à l’aide de l’un des mécanismes normaux. Par exemple, en créant une propriété pour le type d’entité DbSet<TEntity> :

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

    //...

Ou en l’inscrivant dans OnModelCreating:

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

Notes

EntityTypeConfigurationAttribute les types ne seront pas détectés automatiquement dans un assembly. Les types d’entités doivent être ajoutés au modèle avant que l’attribut ne soit découvert sur ce type d’entité.

Améliorations apportées à la création de modèles

En plus des nouveaux attributs de mappage, EF Core 6.0 contient plusieurs autres améliorations apportées au processus de génération de modèle.

Prise en charge des colonnes éparses SQL Server

Problème GitHub : #8023.

SQL Server colonnes éparses sont des colonnes ordinaires optimisées pour stocker des valeurs Null. Cela peut être utile lors de l’utilisation du mappage d’héritage TPH où les propriétés d’un sous-type rarement utilisé entraînent des valeurs de colonne Null pour la plupart des lignes de la table. Par exemple, considérez une ForumModerator classe qui s’étend de ForumUser:

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

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

Il peut y avoir des millions d’utilisateurs, avec seulement quelques-uns de ces modérateurs. Cela signifie que le mappage de l’assémise ForumName peut être judicieux ici. Cela peut maintenant être configuré à l’aide IsSparse de OnModelCreating. Par exemple :

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

Les migrations EF Core marquent ensuite la colonne comme éparse. Par exemple :

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

Notes

Les colonnes éparses ont des limitations. Veillez à lire la documentation des colonnes éparses SQL Server pour vous assurer que les colonnes éparses sont le bon choix pour votre scénario.

Améliorations apportées à l’API HasConversion

Problème GitHub : #25468.

Avant EF Core 6.0, les surcharges génériques des HasConversion méthodes utilisaient le paramètre générique pour spécifier le type à convertir. Par exemple, considérez une Currency énumération :

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core peut être configuré pour enregistrer des valeurs de cette énumération en tant que chaînes « UsDollars », « PoundsStirling » et « Euros » à l’aide HasConversion<string>de . Par exemple :

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

À compter d’EF Core 6.0, le type générique peut à la place spécifier un type de convertisseur de valeur. Il peut s’agir de l’un des convertisseurs de valeur intégrés. Par exemple, pour stocker les valeurs d’énumération sous forme de nombres 16 bits dans la base de données :

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

Ou il peut s’agir d’un type de convertisseur de valeur personnalisé. Par exemple, considérez un convertisseur qui stocke les valeurs d’énumération comme symboles monétaires :

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

Cela peut maintenant être configuré à l’aide de la méthode générique HasConversion :

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

Moins de configuration pour les relations plusieurs-à-plusieurs

Problème GitHub : #21535.

Les relations plusieurs-à-plusieurs non ambiguës entre deux types d’entités sont découvertes par convention. Si nécessaire ou si vous le souhaitez, les navigations peuvent être spécifiées explicitement. Par exemple :

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

Dans ces deux cas, EF Core crée une entité partagée typée en fonction Dictionary<string, object> de l’entité de jointure entre les deux types. À compter d’EF Core 6.0, UsingEntity peut être ajouté à la configuration pour modifier uniquement ce type, sans avoir besoin d’une configuration supplémentaire. Par exemple :

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

En outre, le type d’entité de jointure peut être configuré de manière supplémentaire sans avoir à spécifier explicitement les relations gauche et droite. Par exemple :

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

Enfin, la configuration complète peut être fournie. Par exemple :

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

Autoriser les convertisseurs de valeurs à convertir des valeurs Null

Problème GitHub : #13850.

Important

En raison des problèmes décrits ci-dessous, les constructeurs qui ValueConverter autorisent la conversion de null ont été marqués [EntityFrameworkInternal] pour la version EF Core 6.0. L’utilisation de ces constructeurs génère désormais un avertissement de build.

Les convertisseurs de valeurs n’autorisent généralement pas la conversion de null en une autre valeur. Cela est dû au fait que le même convertisseur de valeur peut être utilisé pour les types nullables et non nullables, ce qui est très utile pour les combinaisons PK/FK où le FK est souvent nullable et le PK n’est pas.

À compter d’EF Core 6.0, un convertisseur de valeur peut être créé qui convertit les valeurs Null. Toutefois, la validation de cette fonctionnalité a révélé qu’elle était très problématique dans la pratique avec de nombreux pièges. Par exemple :

Ces problèmes ne sont pas trivials et pour les problèmes de requête, ils ne sont pas faciles à détecter. Par conséquent, nous avons marqué cette fonctionnalité comme interne pour EF Core 6.0. Vous pouvez toujours l’utiliser, mais vous obtiendrez un avertissement du compilateur. L’avertissement peut être désactivé à l’aide #pragma warning disable EF1001de .

L’un des exemples d’utilisation de la conversion de null peut être utile lorsque la base de données contient des valeurs Null, mais que le type d’entité souhaite utiliser une autre valeur par défaut pour la propriété. Par exemple, considérez une énumération où sa valeur par défaut est « Inconnu » :

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Toutefois, la base de données peut avoir des valeurs null lorsque la race est inconnue. Dans EF Core 6.0, un convertisseur de valeur peut être utilisé pour tenir compte de ce qui suit :

    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
    }

Les chats avec une race de « Inconnu » auront leur Breed colonne définie sur null dans la base de données. Par exemple :

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

Qui génère les instructions d’insertion suivantes sur 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();

Améliorations apportées à la fabrique DbContext

AddDbContextFactory inscrit également DbContext directement

Problème GitHub : #25164.

Parfois, il est utile d’avoir à la fois un type DbContext et une fabrique pour les contextes de ce type tous deux inscrits dans le conteneur d’injection de dépendances d’applications (D.I.). Cela permet, par exemple, une instance délimitée de dbContext d’être résolue à partir de l’étendue de la requête, tandis que la fabrique peut être utilisée pour créer plusieurs instances indépendantes si nécessaire.

Pour le prendre en charge, AddDbContextFactory il inscrit également le type DbContext en tant que service étendu. Par exemple, tenez compte de cette inscription dans le conteneur D.I. de l’application :

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

Avec cette inscription, la fabrique peut être résolue à partir du conteneur D.I. racine, comme dans les versions précédentes :

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

Notez que les instances de contexte créées par la fabrique doivent être supprimées explicitement.

En outre, une instance DbContext peut être résolue directement à partir d’une étendue de conteneur :

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

Dans ce cas, l’instance de contexte est supprimée lorsque l’étendue du conteneur est supprimée ; le contexte ne doit pas être supprimé explicitement.

Au niveau supérieur, cela signifie que dbContext de la fabrique peut être injecté dans d’autres types D.I. Par exemple :

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

Ou :

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 ignore le constructeur sans paramètre DbContext

Problème GitHub : #24124.

EF Core 6.0 permet désormais à la fois un constructeur DbContext sans paramètre et un constructeur qui doit DbContextOptions être utilisé sur le même type de contexte lorsque la fabrique est inscrite via AddDbContextFactory. Par exemple, le contexte utilisé dans les exemples ci-dessus contient les deux constructeurs :

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

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<Blog> Blogs { get; set; }
}

Le regroupement DbContext peut être utilisé sans injection de dépendances

Problème GitHub : #24137.

Le PooledDbContextFactory type a été rendu public afin qu’il puisse être utilisé comme pool autonome pour les instances DbContext, sans que votre application ait besoin d’un conteneur d’injection de dépendances. Le pool est créé avec une instance de DbContextOptions ce qui sera utilisée pour créer des instances de contexte :

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

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

La fabrique peut ensuite être utilisée pour créer et mettre en pool des instances. Par exemple :

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

Les instances sont retournées au pool lorsqu’elles sont supprimées.

Améliorations diverses

Enfin, EF Core contient plusieurs améliorations dans les domaines non abordés ci-dessus.

Utiliser [ColumnAttribute.Order] lors de la création de tables

Problème GitHub : #10059.

La Order propriété de peut désormais être utilisée pour commander des colonnes lors de ColumnAttribute la création d’une table avec des migrations. Par exemple, tenez compte du modèle suivant :

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

Par défaut, EF Core commande d’abord les colonnes clés primaires, en suivant les propriétés du type d’entité et des types appartenant, et enfin les propriétés des types de base. Par exemple, le tableau suivant est créé sur 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]));

Dans EF Core 6.0, ColumnAttribute vous pouvez utiliser pour spécifier un ordre de colonne différent. Par exemple :

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

Sur SQL Server, la table générée est maintenant :

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

Cela déplace les colonnes et LastName les FistName colonnes en haut, même si elles sont définies dans un type de base. Notez que les valeurs de l’ordre des colonnes peuvent avoir des lacunes, ce qui permet aux plages d’être utilisées pour toujours placer des colonnes à la fin, même lorsqu’elles sont utilisées par plusieurs types dérivés.

Cet exemple montre également comment le même ColumnAttribute peut être utilisé pour spécifier à la fois le nom de la colonne et l’ordre.

L’ordre des colonnes peut également être configuré à l’aide de l’API ModelBuilder dans OnModelCreating. Par exemple :

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’ordre sur le générateur de modèles avec HasColumnOrder priorité sur n’importe quel ordre spécifié avec ColumnAttribute. Cela signifie HasColumnOrder que vous pouvez utiliser pour remplacer l’ordre effectué avec des attributs, y compris la résolution des conflits lorsque les attributs sur différentes propriétés spécifient le même numéro de commande.

Important

Notez que, dans le cas général, la plupart des bases de données prennent uniquement en charge l’ordre des colonnes lorsque la table est créée. Cela signifie que l’attribut de l’ordre des colonnes ne peut pas être utilisé pour réorganiser les colonnes dans une table existante. Une exception notable à ceci est SQLite, où les migrations régénèreront l’ensemble de la table avec de nouvelles commandes de colonne.

API MINIMALE EF Core

Problème GitHub : #25192.

.NET Core 6.0 inclut des modèles mis à jour qui simplifient les « API minimales » qui suppriment beaucoup de code réutilisable traditionnellement nécessaire dans les applications .NET.

EF Core 6.0 contient une nouvelle méthode d’extension qui inscrit un type DbContext et fournit la configuration pour un fournisseur de base de données dans une seule ligne. Par exemple :

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

Celles-ci sont exactement équivalentes à :

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"));
var builder = WebApplication.CreateBuilder(args);

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

Notes

Les API minimales EF Core prennent uniquement en charge l’inscription et la configuration de base d’un dbContext et d’un fournisseur. Utilisez AddDbContext, , AddDbContextPool, , AddDbContextFactoryetc. pour accéder à tous les types d’inscription et de configuration disponibles dans EF Core.

Consultez ces ressources pour en savoir plus sur les API minimales :

Conserver le contexte de synchronisation dans SaveChangesAsync

Problème GitHub : #23971.

Nous avons modifié le code EF Core dans la version 5.0 pour définir Task.ConfigureAwait sur false tous les endroits où nous async code await . Il s’agit généralement d’un meilleur choix pour l’utilisation d’EF Core. Toutefois, il s’agit d’un cas particulier, SaveChangesAsync car EF Core définit des valeurs générées dans des entités suivies une fois l’opération de base de données asynchrone terminée. Ces modifications peuvent ensuite déclencher des notifications qui, par exemple, peuvent avoir à s’exécuter sur le thread U.I. Par conséquent, nous rétablissons cette modification dans EF Core 6.0 pour la SaveChangesAsync méthode uniquement.

Base de données en mémoire : valider les propriétés requises ne sont pas null

Problème GitHub : #10613. Cette fonctionnalité a été apportée par @fagnercarvalho. Merci beaucoup !

La base de données en mémoire EF Core lève désormais une exception si une tentative d’enregistrement d’une valeur Null est effectuée pour une propriété marquée comme obligatoire. Par exemple, considérez un User type avec une propriété obligatoire Username :

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

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

Toute tentative d’enregistrement d’une entité avec une valeur Null Username entraîne l’exception suivante :

Microsoft.EntityFrameworkCore.DbUpdateException : Les propriétés requises « {« Username » } sont manquantes pour l’instance de type d’entité « User » avec la valeur de clé « {Id : 1} ».

Cette validation peut être désactivée si nécessaire. Par exemple :

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

Informations de source de commande pour les diagnostics et les intercepteurs

Problème GitHub : #23719. Cette fonctionnalité a été apportée par @Giorgi. Merci beaucoup !

L’élément CommandEventData fourni aux sources de diagnostic et aux intercepteurs contient désormais une valeur d’énumération indiquant quelle partie d’EF a été chargée de créer la commande. Cela peut être utilisé comme filtre dans les diagnostics ou l’intercepteur. Par exemple, nous pouvons souhaiter un intercepteur qui s’applique uniquement aux commandes provenant de 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;
    }
}

Cela filtre l’intercepteur sur les événements uniquement SaveChanges lorsqu’il est utilisé dans une application qui génère également des migrations et des requêtes. Par exemple :

Saving changes for CustomersContext:

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

Meilleure gestion des valeurs temporaires

Problème GitHub : #24245.

EF Core n’expose pas de valeurs temporaires sur les instances de type d’entité. Par exemple, considérez un type d’entité Blog avec une clé générée par le magasin :

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

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

La Id propriété de clé obtient une valeur temporaire dès qu’une Blog valeur est suivie par le contexte. Par exemple, lors de l’appel DbContext.Add:

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

La valeur temporaire peut être obtenue à partir du suivi des modifications de contexte, mais n’est pas définie dans l’instance d’entité. Par exemple, ce code :

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

Voici la sortie générée :

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

Cela est bon, car il empêche la fuite de valeur temporaire dans le code de l’application où elle peut être traitée accidentellement comme non temporaire. Toutefois, il est parfois utile de traiter directement les valeurs temporaires. Par exemple, une application peut souhaiter générer ses propres valeurs temporaires pour un graphique d’entités avant qu’elles ne soient suivies afin qu’elles puissent être utilisées pour former des relations à l’aide de clés étrangères. Pour ce faire, vous pouvez marquer explicitement les valeurs comme temporaires. Par exemple :

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

Dans EF Core 6.0, la valeur reste sur l’instance d’entité, même si elle est maintenant marquée comme temporaire. Par exemple, le code ci-dessus génère la sortie suivante :

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

De même, les valeurs temporaires générées par EF Core peuvent être définies explicitement sur les instances d’entité et marquées comme des valeurs temporaires. Cela peut être utilisé pour définir explicitement des relations entre de nouvelles entités à l’aide de leurs valeurs de clé temporaires. Par exemple :

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

Ce qui donne :

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 annoté pour les types référence nullableS C#

Problème GitHub : #19007.

La base de code EF Core utilise désormais des types référence nullable C# (NRT) dans l’ensemble. Cela signifie que vous obtiendrez les indications correctes du compilateur pour une utilisation null lors de l’utilisation d’EF Core 6.0 à partir de votre propre code.

Microsoft.Data.Sqlite 6.0

Conseil

Vous pouvez exécuter et déboguer dans tous les exemples présentés ci-dessous en téléchargeant l’exemple de code à partir de GitHub.

Regroupement de connexions

Problème GitHub : #13837.

Il est courant de garder les connexions de base de données ouvertes le plus peu de temps possible. Cela permet d’empêcher la contention sur la ressource de connexion. C’est pourquoi les bibliothèques comme EF Core ouvrent la connexion immédiatement avant d’effectuer une opération de base de données et la ferment immédiatement après. Par exemple, considérez ce code 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.");
    }
}

La sortie de ce code, avec la journalisation des connexions activées, est la suivante :

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.

Notez que la connexion est ouverte et fermée rapidement pour chaque opération.

Toutefois, pour la plupart des systèmes de base de données, l’ouverture d’une connexion physique à la base de données est une opération coûteuse. Par conséquent, la plupart des fournisseurs ADO.NET créent un pool de connexions physiques et les louent aux DbConnection instances en fonction des besoins.

SQLite est un peu différent, car l’accès à la base de données n’accède généralement qu’à un fichier. Cela signifie que l’ouverture d’une connexion à une base de données SQLite est généralement très rapide. Toutefois, ce n'est pas toujours le cas. Par exemple, l’ouverture d’une connexion à une base de données chiffrée peut être très lente. Par conséquent, les connexions SQLite sont désormais mises en pool lors de l’utilisation de Microsoft.Data.Sqlite 6.0.

Prise en charge de DateOnly et TimeOnly

Problème GitHub : #24506.

Microsoft.Data.Sqlite 6.0 prend en charge les nouveaux DateOnly et TimeOnly types de .NET 6. Ceux-ci peuvent également être utilisés dans EF Core 6.0 avec le fournisseur SQLite. Comme toujours avec SQLite, son système de type natif signifie que les valeurs de ces types doivent être stockées en tant que l’un des quatre types pris en charge. Microsoft.Data.Sqlite les stocke en tant que TEXT. Par exemple, une entité utilisant ces types :

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

Mappe au tableau suivant dans la base de données 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);

Les valeurs peuvent ensuite être enregistrées, interrogées et mises à jour de la manière normale. Par exemple, cette requête EF Core LINQ :

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

Est traduit en ce qui suit sur SQLite :

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

Et retourne uniquement les utilisations avec les anniversaires avant 1900 CE :

Found 'ajcvickers'
Found 'wendy'

Savepoints API

Problème GitHub : #20228.

Nous avons standardisé sur une API commune pour les points d’enregistrement dans ADO.NET fournisseurs. Microsoft.Data.Sqlite prend désormais en charge cette API, notamment :

L’utilisation d’un point d’enregistrement permet de restaurer une partie d’une transaction sans restaurer l’intégralité de la transaction. Par exemple, le code ci-dessous :

  • Crée une transaction
  • Envoie une mise à jour à la base de données
  • Crée un point d’enregistrement
  • Envoie une autre mise à jour à la base de données
  • Rétablit le point d’enregistrement précédent créé
  • Valide la transaction
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();

Cela entraîne la validation de la première mise à jour dans la base de données, tandis que la deuxième mise à jour n’est pas validée, car le point d’enregistrement a été restauré avant de valider la transaction.

Délai d’expiration de la commande dans la chaîne de connexion

Problème GitHub : #22505. Cette fonctionnalité a été apportée par @nmichels. Merci beaucoup !

ADO.NET fournisseurs prennent en charge deux délais d’expiration distincts :

  • Délai d’expiration de la connexion, qui détermine la durée maximale d’attente lors de la connexion à la base de données.
  • Délai d’expiration de la commande, qui détermine la durée maximale d’attente de l’exécution d’une commande.

Le délai d’expiration de la commande peut être défini à partir du code à l’aide DbCommand.CommandTimeoutde . De nombreux fournisseurs exposent désormais ce délai d’expiration de commande dans la chaîne de connexion. Microsoft.Data.Sqlite suit cette tendance avec le Command Timeout mot clé de chaîne de connexion. Par exemple, "Command Timeout=60;DataSource=test.db" utilise 60 secondes comme délai d’expiration par défaut pour les commandes créées par la connexion.

Conseil

Sqlite traite Default Timeout comme un synonyme et Command Timeout peut donc être utilisé à la place si vous le préférez.