Nouveautés de EF Core 6.0

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

Conseil

Vous pouvez exécuter et déboguer les exemples repris ci-dessous en téléchargeant l'exemple de code depuis GitHub.

Tables temporelles SQL Server

Problème GitHub : #4693.

Les tables temporelles SQL Server assurent 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 possible 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 l’interrogation des données historiques, à des fins d’audit, par exemple, ou leur restauration, par exemple pour la récupération après une mutation ou une suppression accidentelle.

EF Core prend désormais en charge :

  • Création de tables temporelles à l’aide de migrations
  • Transformation de tables existantes en tables temporelles, à nouveau à l’aide de migrations
  • Interrogation des données historiques
  • Restauration de données à partir d’un point antérieur 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());

En cas d’utilisation de EF Core pour créer la base de données, la nouvelle table est configurée en tant que table temporelle avec les valeurs SQL Server par défaut pour les horodatages et la table d’historique. Prenons l’exemple d’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 se présente comme suit :

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 datetime2 masquées nommées PeriodEnd et PeriodStart. Ces « colonnes de période » représentent l’intervalle de temps pendant lequel les données de la ligne existent. Ces colonnes sont mappées aux propriétés cachées dans le modèle EF Core, ce qui permet de les utiliser dans des requêtes comme indiqué plus tard.

Important

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

Notez également qu’une table d’historique associée nommé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 pour le 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");
            }));

Cela se reflète 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, de sorte que l’application peut les ignorer. Par exemple, les nouvelles entités peuvent être enregistrées dans la base de données de manière habituelle :

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 manière habituelle. 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 peuvent être 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

La colonne ValidTo (nommée par défaut PeriodEnd) contient la valeur maximale datetime2. C’est toujours le cas pour les lignes actuelles de la table. Les colonnes ValidFrom (nommées par défaut PeriodStart) contiennent l’heure UTC d’insertion de la ligne.

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 les lignes qui étaient actives (actuelles) à l’heure UTC donnée. Il s’agit d’une ligne unique de la table actuelle ou de la table d’historique pour une clé primaire donnée.
  • TemporalAll : retourne toutes les lignes dans les 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 qui étaient 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 pour une clé primaire donnée.
  • TemporalBetween : identique à TemporalFromTo, sauf que les lignes qui sont devenues actives sur la limite supérieure sont incluses.
  • TemporalContainedIn : retourne toutes les lignes qui ont commencé à être actives et qui se sont terminées pendant deux heures UTC données. Il peut s’agir de nombreuses lignes de la table d’historique et/ou de la table actuelle pour une clé primaire donnée.

Remarque

Pour plus d’informations sur la façon dont les lignes sont incluses pour chacun de ces opérateurs, consultez la documentation sur les tables temporelles SQL Server.

Par exemple, après avoir apporté des mises à jour et des suppressions à nos données, nous pouvons exécuter une requête à l’aide de TemporalAll pour afficher les 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 la méthode EF.Property peut être utilisée pour accéder aux valeurs des colonnes de période. Elle est utilisée dans la clause OrderBy 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 le 26/08/2021 à 4:44:59 PM. Cela est dû au fait que la ligne relative à 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 de TemporalFromTo, de TemporalBetween ou de 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 indiqué ci-dessus, Rainbow Dash a été supprimé de la table Employees. Comme il s’agit clairement d’une erreur, nous allons revenir à un point dans le temps et restaurer la ligne manquante à partir de ce moment-là.

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

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

Cette requête retourne une ligne unique pour Rainbow Dash telle qu’elle était à l’heure UTC donnée. Toutes les requêtes qui utilisent des opérateurs temporels sont sans suivi par défaut. L’entité retournée ici n’est donc pas suivie. Cela est logique, car elle n’existe pas actuellement dans la table principale. Pour réinsérer l’entité dans la table principale, il vous suffit de la marquer comme Added, puis d’appeler SaveChanges.

Après avoir réinséré la ligne Rainbow Dash, l’interrogation des données historiques indique que la ligne a été restaurée telle qu’elle était à l’heure UTC donnée :

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

Packs de migration

Problème GitHub : #19693.

Les migrations EF Core sont utilisées pour générer des mises à jour du schéma de base de données en fonction des modifications apportées au modèle EF. Ces mises à jour du 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/déploiement continu (C.I./C.D.).

EF Core propose désormais une nouvelle façon d’appliquer ces mises à jour du schéma, à savoir les packs de migration. Un pack de migration est un petit exécutable contenant des migrations et le code nécessaire pour appliquer ces migrations à la base de données.

Remarque

Pour plus d’informations sur les migrations, les packs et le déploiement, consultez l’article Introducing DevOps-friendly EF Core Migration Bundles sur le blog .NET.

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

Un pack a besoin de migrations à inclure. Celles-ci sont créées à l’aide de la commande dotnet ef migrations add décrite dans la documentation relative aux migrations. Une fois que vous avez des migrations prêtes à être déployées, créez un pack à l’aide de dotnet ef migrations bundle. 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 un fichier efbundle.exe dans mon dossier local. L’exécution de cet exécutable applique les migrations contenues dans celui-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, la nouvelle exécution du même pack 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 nouvel exécutable prêt à 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 pack existant par un nouveau.

L’exécution de ce nouveau pack 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 pack 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 transmettant 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 transmises à 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 quand le projet contient plusieurs types de contexte.
  • --project pour spécifier le projet à utiliser. Par défaut, il s’agit du répertoire de travail actuel.
  • --startup-project pour spécifier le projet de démarrage à utiliser. Par défaut, il s’agit du répertoire de travail actuel.
  • --no-build pour empêcher la génération du projet avant d’exécuter la commande. Cette option ne doit être utilisée que si le projet est à jour.
  • --verbose pour obtenir des informations détaillées sur l’action de la commande. Utilisez cette option quand vous incluez des informations dans des rapports de bogue.

Utilisez dotnet ef migrations bundle --help pour afficher toutes les options disponibles.

Notez que, par défaut, chaque migration est appliquée 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 antérieures de EF Core exigent que le mappage de chaque propriété d’un type donné soit configuré de manière explicite quand ce mappage diffère de la valeur par défaut. Cela comprend des « facettes », telles que la longueur maximale des chaînes et la précision décimale, ainsi que la conversion des valeurs pour le type de propriété.

Cela nécessitait :

  • Une configuration du générateur de modèles pour chaque propriété
  • Un attribut de mappage sur chaque propriété
  • Une itération explicite sur toutes les propriétés de tous les types d’entité et l’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 à réaliser de manière robuste, car la liste des types d’entités et des propriétés mappées peut ne pas être définitive au moment où cette itération se produit.

EF Core 6.0 autorise la spécification unique de cette configuration de mappage pour un type donné. Elle est 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 génération de modèle. Cette configuration est appliquée en substituant ConfigureConventions sur votre type DbContext :

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

Par exemple, considérons 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 au format ANSI (au lieu de Unicode) et avoir une longueur maximale de 1024 :

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 à l’aide de la conversion par défaut de DateTimes en valeurs longues :

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

Toutes les propriétés booléennes 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 que Session est une propriété temporaire de l’entité et ne doit pas être rendue persistante, 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 quand vous utilisez des objets de 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 à partir de 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 valeurs 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 code 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);

Ce cas est rarement nécessaire, mais il peut être utile si un type est utilisé dans une requête d’une manière non corrélée avec une propriété mappée du modèle.

Remarque

Pour plus d’informations et des exemples de configuration du modèle de pré-convention, consultez l’article Announcing Entity Framework Core 6.0 Preview 6: Configure Conventions sur le blog .NET.

Modèles compilés

Problème GitHub : #1906.

Les modèles compilés peuvent améliorer le temps de démarrage de EF Core pour les applications avec des modèles volumineux. Un modèle volumineux implique généralement des centaines voire des milliers de types d’entités et de relations.

Le temps de démarrage correspond à la durée d’exécution de la première opération sur un type DbContext quand 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 de 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 en ligne de commande dotnet ef. Vérifiez que vous avez installé la dernière version de l’outil avant de continuer.

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

dotnet ef dbcontext optimize

Les options --output-dir et --namespace 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 à copier-coller dans votre configuration de DbContext pour que 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");

Amorçage du modèle compilé

En général, il n’est pas nécessaire d’examiner le code d’amorçage généré. Toutefois, il peut parfois être utile de personnaliser le modèle ou son chargement. Le code d’amorçage se présente comme suit :

[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 certaines configurations d’exécution. Ils 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é si nécessaire. 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 des modèles compilés que si le temps de démarrage de EF Core est trop lent. La compilation de petits modèles ne vaut généralement pas la peine.

Si la prise en charge de 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’y exécuter un point de référence 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, 6 390 propriétés et 720 relations. Il s’agit d’un modèle modérément volumineux. En utilisant BenchmarkDotNet pour la mesure, 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 permet d’atteindre 117 millisecondes sur le même matériel. Une amélioration de 8 à 10 fois plus telle que celle-ci reste relativement constante à mesure que la taille du modèle augmente.

Compiled model performance improvement

Remarque

Pour plus d’informations sur les modèles compilés et les performances de démarrage EF Core, consultez l’article Announcing Entity Framework Core 6.0 Preview 5: Compiled Models sur le blog .NET.

Amélioration des performances sur TechEmpower Fortunes

Problème GitHub : #23611.

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

  • Les performances de EF Core 6.0 sont désormais 70 % plus rapides sur le point de référence TechEmpower Fortunes standard par rapport à la version 5.0.
    • Il s’agit de l’amélioration des performances de la pile complète, y compris des améliorations apportées au code de point de référence, au runtime .NET, etc.
  • EF Core 6.0 exécute les requêtes non suivies selon une vitesse supérieure de 31 %.
  • Les allocations de tas ont été réduites de 43 % lors de l’exécution des requêtes.

Après ces améliorations, le fossé entre le Dapper « micro-ORM » populaire et EF Core dans le point de référence TechEmpower Fortunes est passé de 55 % à environ 5 %.

Remarque

Pour obtenir des informations détaillées sur les améliorations des performances des requêtes dans EF Core 6.0, consultez l’article Announcing Entity Framework Core 6.0 Preview 4: Performance Edition sur le blog .NET.

Améliorations du fournisseur Azure Cosmos DB

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

Conseil

Vous pouvez exécuter et déboguer 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 création d’un modèle pour le fournisseur Azure Cosmos DB, 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 OwnsMany et OwnsOne dans le modèle Azure Cosmos DB. Cela facilite l’incorporation de types enfants dans le document pour le type parent, qui est généralement la méthode appropriée pour modéliser des parents et des enfants dans une base de données de documents.

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

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 Azure Cosmos DB 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 Azure Cosmos DB résultants ont les parents, les enfants, les animaux de compagnie et l’adresse de la famille incorporés dans le document de la 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
}

Remarque

Il est important de se souvenir que la configuration OwnsOne/OwnsMany doit être utilisée si vous avez besoin de 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 Azure Cosmos DB. 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 en fonctions intégrées

Problème GitHub : #16143.

Le fournisseur Azure Cosmos DB traduit désormais davantage de méthodes de bibliothèque de classes de base (BCL) en fonctions intégrées Azure Cosmos DB. Les tableaux ci-dessous affichent les nouvelles traductions dans EF Core 6.0.

Traductions String

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 Appels sans respect de la casse uniquement

@Marusyk a contribué aux traductions pour LOWER, LTRIM, RTRIM, TRIM, UPPER et SUBSTRING. Merci !

Par exemple :

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

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

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

@Marusyk a contribué à ces traductions. Merci !

Par exemple :

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

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

@Marusyk a contribué à ces traductions. Merci !

Par exemple :

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

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.

Il est parfois nécessaire d’exécuter une requête SQL brute au lieu d’utiliser LINQ. Cette fonctionnalité est désormais prise en charge avec le fournisseur Azure Cosmos DB à l’aide de la méthode FromSql. Elle fonctionne de la même manière qu’avec les fournisseurs relationnels. Par exemple :

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

Qui est exécuté comme suit :

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

Requêtes distinctes

Problème GitHub : #16144.

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

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

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 Azure Cosmos DB journalise 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 quand c’est nécessaire.

Remarque

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

L’insertion d’un élément dans la base de données Azure Cosmos DB 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();

Journalise 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 Azure Cosmos DB à l’aide d’une requête génère l’événement CosmosEventId.ExecutingSqlQuery, puis un ou plusieurs événements CosmosEventId.ExecutedReadNext pour les éléments lus. Par exemple, ce code :

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

Journalise 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 Azure Cosmos DB à l’aide de Find avec une clé de partition génère les événements CosmosEventId.ExecutingReadItem et CosmosEventId.ExecutedReadItem. Par exemple, ce code :

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

Journalise 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 Azure Cosmos DB génère l’événement CosmosEventId.ExecutedReplaceItem. Par exemple, ce code :

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

Journalise 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 Azure Cosmos DB génère l’événement CosmosEventId.ExecutedDeleteItem. Par exemple, ce code :

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

Journalise 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 Azure Cosmos DB peut désormais être configuré avec un débit manuel ou 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 Azure Cosmos DB peuvent désormais être configurés avec la durée de vie par défaut et la durée de vie pour le magasin analytique. Par exemple :

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

Résoudre la fabrique de clients HTTP

Problème GitHub : #21274. @dnperfors a contribué à cette fonctionnalité. Merci !

L’élément HttpClientFactory utilisé par le fournisseur Azure Cosmos DB peut désormais être défini explicitement. Cela peut être particulièrement utile lors de tests, par exemple pour contourner la validation de certificats lors de l’utilisation de l’émulateur Azure Cosmos DB sur Linux :

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

Remarque

Pour obtenir un exemple détaillé d’application des améliorations apportées au fournisseur Azure Cosmos DB à une application existante, consultez l’article Taking the EF Core Azure Cosmos DB Provider for a Test Drive sur le blog .NET.

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

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

Génération de modèles 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 ces tables. Par exemple, considérez les tables pour Posts et Tags, et une table de jointure PostTag les connectant :

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

Générer automatiquement des types références Nullables C#

Problème GitHub : #15520.

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

Par exemple, la table Tags suivante contient des colonnes de chaîne pouvant accepter ou non la valeur Null :

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

Cela génère des propriétés de chaîne nullable et non-nullable 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, la table Posts suivante contient une relation requise avec la table Blogs :

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 de modèles automatique de 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 les publications :

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 type DbContext généré sont créées de manière à accepter les 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. @ErikEJ a contribué à cette fonctionnalité. Merci !

Les commentaires sur les tables et les colonnes SQL sont désormais générés automatiquement dans les types d’entités créés lors de l’ingénierie à rebours 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 des 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 fournit une meilleure prise en charge des requêtes GroupBy. En particulier, EF Core :

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

Voici des exemples de requêtes de rapports de client et 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. @wmeints a contribué à cette fonctionnalité. Merci !

À compter de EF Core 6.0, les appels de String.Concat avec plusieurs arguments sont désormais traduits en code SQL. Par exemple, dans la requête qui suit :

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

Se traduit par le code SQL suivant quand vous utilisez 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 souple à 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 antérieures de EF Core était fastidieuse en raison d’un conflit d’espace de noms pour les méthodes LINQ asynchrones. Dans EF Core 6.0, nous avons tiré parti de la correspondance de modèle C# pour IAsyncEnumerable<T> afin que la fonction EF Core DbSet<TEntity> exposée n’ait pas besoin d’implémenter l’interface directement.

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 en matière de paramètres pour les fonctions FreeText(DbFunctions, String, String) et Contains. Cela permet d’utiliser ces fonctions 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 propriété Name définie en tant qu’objet de 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; }
}

Cette valeur est mappée au format 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 de Contains ou de FreeText même si le type de la propriété est Name, pas string. Par exemple :

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

Cette requête 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. @ralmsdeveloper a contribué à cette fonctionnalité. Merci !

Les appels de ToString() sont désormais traduits en code 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 qui ne sont pas des chaînes. Par exemple, considérez un type d’entité User qui stocke les numéros de téléphone comme 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 numéro 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 numéros qui correspondent à un modèle. Par exemple, pour rechercher tous les numéros contenant 555 :

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

Cette requête se traduit par le code SQL suivant quand vous utilisez 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%'

La traduction de ToString() pour 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. @RaymondHuy a contribué à cette fonctionnalité. Merci !

EF.Functions.Random mappe à 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 référentiel EF Core pour SQL Server, SQLite et Azure Cosmos DB. Par exemple, considérez un type d’entité User avec une propriété Popularity :

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 de EF.Functions.Random, 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();

Cette requête se traduit par le code SQL suivant quand vous utilisez 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. @Marusyk a contribué à cette fonctionnalité. Merci !

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, cette requête était traduite comme suit dans 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 en :

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 le 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 très utile pour créer l’équivalent des vues sur la base de données en mémoire, en particulier quand ces vues retournent des types d’entité sans clé. Par exemple, considérez une base de données de clients pour les clients établis 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; }
}

Imaginons à présent que nous souhaitons obtenir une vue sur ces données qui indique le nombre de clients dans chaque zone de code postal. Nous pouvons créer un type d’entité sans clé pour représenter cette vue :

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

Et définir une propriété DbSet sur le type 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()
                        }));
}

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

var results = context.CustomerDensities.ToList();

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

Problème GitHub : #20173. @stevendarby a contribué à cette fonctionnalité. Merci !

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

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

Cette requête se traduit par le code SQL suivant quand vous utilisez 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 sans navigation

Problème GitHub : #21234.

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

Voici des exemples de requêtes qui illustrent la traduction sur SQL Server en une requête unique ou en 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]

Requêtes SQL multiples :

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]

Requêtes SQL multiples :

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]

Requêtes SQL multiples :

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 s’assurer 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 que EF génère les regroupements nécessaires et peut avoir un impact sur les performances. Par conséquent, dans 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();

Dans EF Core 5.0 sur SQL Server, cette requête est traduite par :

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]

Dans EF Core 6.0, elle est traduite par :

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 les requêtes avec le nom de fichier et le numéro de ligne

Problème GitHub : #14176. @michalczerwinski a contribué à cette fonctionnalité. Merci !

Les balises de requête permettent d’ajouter une balise de texture à une requête LINQ de sorte qu’elle soit ensuite incluse dans le code SQL généré. Dans EF Core 6.0, cela 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 donne le code SQL généré suivant 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 dépendante facultative détenue

Problème GitHub : #24558.

Il devient difficile de savoir si une entité dépendante facultative existe ou non quand elle partage une table avec son entité principale. Cela est dû au fait qu’il y a une ligne dans la table pour l’entité dépendante, car l’entité principale en a besoin, que l’entité dépendante existe ou non. La façon de gérer cette situation sans ambiguïté consiste à s’assurer que l’entité dépendante a au moins une propriété requise. Dans la mesure où une propriété requise ne peut pas avoir la valeur Null, si la valeur de la colonne pour cette propriété est Null, l’entité dépendante n’existe pas.

Par exemple, imaginons une classe Customer où chaque client a une propriété Address détenue :

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, celle-ci doit avoir au moins un code postal non-null :

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

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

Désormais, quand les clients sont interrogés, si la colonne de code postal a la valeur Null, cela signifie que le client n’a pas d’adresse et que la propriété de navigation Customer.Address a la valeur Null. Par exemple, lors de l’itération au sein des clients et de la vérification 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}.");
    }
}

Les résultats suivants sont générés :

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

Considérez plutôt le cas où aucune propriété 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; }
}

Désormais, il est possible d’enregistrer 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 ne peuvent pas être distingués, comme nous pouvons le constater en interrogeant directement les colonnes de la 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

Par conséquent, EF Core 6.0 vous avertit désormais lors de l’enregistrement d’une entité dépendante facultative dont toutes les propriétés ont la valeur Null. Par exemple :

warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) The entity of type 'Address' with primary key values {CustomerId: -2147482646} is an optional dependent using table sharing. The entity does not have any property with a non-default value to identify whether the entity exists. This means that when it is queried no object instance will be created instead of an instance with all properties set to default values. Any nested dependents will also be lost. Either don't save any instance with only default values or mark the incoming navigation as required in the model.

Cela devient encore plus délicat quand l’entité dépendante facultative elle-même agit en tant qu’entité principale pour une autre entité dépendante facultative, également mappée à la même table. Au lieu d’un simple avertissement, EF Core 6.0 interdit uniquement les cas d’entités dépendantes facultatives imbriquées. Par exemple, considérez le modèle suivant, où ContactInfo appartient à Customer et Address appartient à 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; }
}

Désormais, si ContactInfo.Phone a la valeur 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 contenir des données. Pour ce type de modèle, EF Core 6.0 lève l’exception suivante :

System.InvalidOperationException: Entity type 'ContactInfo' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.

L’objectif ici consiste à éviter le cas où une entité dépendante facultative peut contenir toutes les valeurs de propriété Nullable et partage une table avec son entité principale. Il existe trois façons simples d’éviter cette situation :

  1. Rendre l’entité dépendante requise. 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. S’assurer que l’entité dépendante contient au moins une propriété requise, comme décrit ci-dessus.
  3. Enregistrer les entités dépendantes facultatives dans leur propre table, au lieu de partager une table avec l’entité principale.

Une entité dépendante peut être rendue requise à 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’elle est requise dans OnModelCreating :

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

Les entités dépendantes peuvent être enregistrées 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"));
            });

Consultez la page OptionalDependentsSample dans GitHub pour obtenir d’autres exemples d’entités dépendantes facultatives, notamment des cas avec des entités dépendantes 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. @RaymondHuy a contribué à cette fonctionnalité. Merci !

À compter de EF Core 6.0, une propriété de type 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 la base de données. Par exemple, considérez un type d’entité Book avec une propriété pour le numéro international normalisé du livre (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 numéros 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 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 varchar(22) :

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

Remarque

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

PrecisionAttribute

Problème GitHub : #17914. @RaymondHuy a contribué à cette fonctionnalité. Merci !

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 une précision de 10 et une échelle de 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. @KaloyanIT a contribué à cette fonctionnalité. Merci !

Les instances IEntityTypeConfiguration<TEntity> autorisent la configuration ModelBuilder pour 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>());
}

À partir de EF Core 6.0, un attribut EntityTypeConfigurationAttribute peut être placé sur le type d’entité afin que 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 que EF Core utilise l’implémentation IEntityTypeConfiguration spécifiée chaque fois que le type d’entité Book 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é DbSet<TEntity> pour le type d’entité :

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

Remarque

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

Améliorations de la génération de modèle

Outre les 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.

Les colonnes éparses SQL Server 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 classe ForumModerator 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, dont quelques-uns seulement sont des modérateurs. Cela signifie que le mappage de ForumName comme colonne éparse peut être judicieux ici. Cette situation peut désormais être configurée à l’aide de IsSparse dans 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 étant é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]));

Remarque

Les colonnes éparses présentent des limitations. Veillez à lire la documentation sur les colonnes éparses SQL Server pour vous assurer que les colonnes éparses sont le choix idéal pour votre scénario.

Améliorations de l’API HasConversion

Problème GitHub : #25468.

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

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

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

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

À compter de EF Core 6.0, le type générique peut spécifier un type de convertisseur de valeurs. Il peut s’agir de l’un des convertisseurs de valeurs intégrés. Par exemple, pour stocker les valeurs d’énumération en tant que 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 valeurs personnalisé. Par exemple, imaginez un convertisseur qui stocke les valeurs d’énumération en tant que 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)
    {
    }
}

Cette situation peut désormais être configurée à l’aide de la méthode générique HasConversion :

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

Réduction de la configuration pour les relations plusieurs-à-plusieurs

Problème GitHub : #21535.

Les relations plusieurs-à-plusieurs non ambiguës entre deux types d’entité 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 un type d’entité partagé basé sur Dictionary<string, object> pour agir en tant qu’entité de jointure entre les deux types. À compter de EF Core 6.0, UsingEntity peut être ajouté à la configuration pour modifier uniquement ce type, sans qu’une configuration supplémentaire soit nécessaire. 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é sans avoir à spécifier explicitement les relations de gauche et de 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, vous pouvez fournir la configuration complète. 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 les valeurs null

Problème GitHub : #13850.

Important

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

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

À partir de EF Core 6.0, vous pouvez créer un convertisseur de valeurs qui convertit les valeurs Null. Toutefois, la validation de cette fonctionnalité s’est révélée être très problématique dans la pratique avec de nombreux pièges. Par exemple :

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

Un exemple d’une situation dans laquelle la conversion de valeurs null peut être utile est quand 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, imaginez une énumération dont la valeur par défaut est « Inconnue » :

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Toutefois, la base de données peut avoir des valeurs null quand la race est inconnue. Dans EF Core 6.0, un convertisseur de valeurs peut être utilisé pour prendre en compte 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
    }

Pour les chats dont la race est « Inconnue », la colonne Breed est définie sur la valeur 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();

Ce qui génère les instructions d’insertion suivantes dans 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 de la fabrique DbContext

AddDbContextFactory inscrit également DbContext directement

Problème GitHub : #25164.

Il est parfois utile d’avoir un type DbContext et une fabrique pour les contextes de ce type inscrits dans le conteneur d’injection de dépendances (DI) d’applications. Cela permet, par exemple, de résoudre une instance délimitée de DbContext à partir de l’étendue de la demande, tandis que la fabrique peut être utilisée pour créer plusieurs instances indépendantes si nécessaire.

Pour prendre cette situation en charge, AddDbContextFactory inscrit désormais aussi le type DbContext en tant que service délimité. Par exemple, considérez cette inscription dans le conteneur DI de l’application :

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

Avec cette inscription, la fabrique peut être résolue à partir du conteneur DI racine, comme dans les versions antérieures :

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

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 quand l’étendue du conteneur est supprimée ; le contexte ne doit pas être supprimé explicitement.

À un niveau supérieur, cela signifie que le type DbContext de la fabrique peut être injecté dans d’autres types DI. 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 autorise désormais un constructeur DbContext sans paramètre et un constructeur qui prend DbContextOptions pour être utilisé sur le même type de contexte quand 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; }
}

La mise en pool DbContext peut être utilisée sans injection de dépendances

Problème GitHub : #24137.

Le type PooledDbContextFactory a été rendu public afin de pouvoir être utilisé comme un 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 qui sera utilisée pour créer des instances de contexte :

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

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

La fabrique peut ensuite être utilisée pour créer et regrouper 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 quand elles sont supprimées.

Améliorations diverses

Enfin, EF Core contient plusieurs améliorations dans des domaines non couverts ci-dessus.

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

Problème GitHub : #10059.

La propriété Order de ColumnAttribute peut désormais être utilisée pour classer les colonnes lors de la création d’une table avec des migrations. Par exemple, prenons le 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 trie d’abord les colonnes de clés primaires, suivies des propriétés du type d’entité et des types détenus, et enfin les propriétés à partir des types de base. Par exemple, le tableau suivant est créé dans 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 peut être utilisé 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; }
}

Dans SQL Server, la table générée est désormais :

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

Les colonnes FistName et LastName sont déplacées vers le haut, même si elles sont définies dans un type de base. Les valeurs d’ordre des colonnes peuvent avoir des écarts, ce qui permet d’utiliser des plages pour toujours placer des colonnes à la fin, même quand elles sont utilisées par plusieurs types dérivés.

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

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 est prioritaire sur n’importe quel ordre spécifié avec ColumnAttribute. Par conséquent, HasColumnOrder peut être utilisé pour remplacer le classement effectué avec des attributs, y compris pour résoudre les conflits quand des attributs de propriétés différentes spécifient le même numéro d’ordre.

Important

En général, la plupart des bases de données prennent uniquement en charge le classement des colonnes lors de la création de la table. Ainsi, l’attribut d’ordre des colonnes ne peut pas être utilisé pour réorganiser les colonnes d’une table existante. Une exception notable est SQLite, où les migrations régénèrent la table entière avec le nouvel ordre des colonnes.

API minimale EF Core

Problème GitHub : #25192.

.NET Core 6.0 comprend des modèles mis à jour qui présentent des « API minimales » simplifiées qui suppriment une grande partie du 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 d’un fournisseur de base de données sur 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==");

Ces éléments sont exactement équivalents à :

var builder = WebApplication.CreateBuilder(args);

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

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

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

Remarque

Les API minimales EF Core prennent uniquement en charge l’inscription et la configuration très basiques d’un type DbContext et d’un fournisseur. Utilisez AddDbContext, AddDbContextPool, AddDbContextFactory, etc., pour accéder à tous les types d’inscription et de configuration disponibles dans EF Core.

Consultez les ressources ci-dessous 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 emplacements où du code asynchrone est await. Il s’agit généralement d’un meilleur choix pour l’utilisation de EF Core. Toutefois, SaveChangesAsync est un cas particulier, car EF Core définit les valeurs générées en 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, doivent être exécutées sur le thread UI. Par conséquent, nous rétablissons cette modification dans EF Core 6.0 pour la méthode SaveChangesAsync uniquement.

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

Problème GitHub : #10613. @fagnercarvalho a contribué à cette fonctionnalité. Merci !

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

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

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

Si vous tentez d’enregistrer une entité avec une propriété Username Null, l’exception suivante se produit :

Microsoft.EntityFrameworkCore.DbUpdateException : les propriétés requises « {'username'} » sont manquantes pour l’instance du 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 sur la source de commande pour les diagnostics et les intercepteurs

Problème GitHub : #23719. @Giorgi a contribué à cette fonctionnalité. Merci !

L’argument CommandEventData fourni aux sources de diagnostic et aux intercepteurs contient désormais une valeur d’énumération qui indique quelle partie de EF est responsable de la création de la commande. Vous pouvez l’utiliser comme filtre dans les diagnostics ou l’intercepteur. Par exemple, nous souhaitons 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 permet de filtrer l’intercepteur uniquement pour les événements SaveChanges quand ’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();

Amélioration de la 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 propriété de clé Id obtient une valeur temporaire dès qu’une entité Blog est suivie par le contexte. Par exemple, lors de l’appel de DbContext.Add :

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

La valeur temporaire peut être obtenue à partir du dispositif de 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

Cette pratique est utile, car elle empêche la valeur temporaire de se dévoiler dans le code de l’application où elle peut être traitée par inadvertance 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 étant 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 désormais marquée comme étant 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 des instances d’entité et marquées en tant que valeurs temporaires. Ce principe 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

Annotation EF Core pour les types références Nullables C#

Problème GitHub : #19007.

Le codebase EF Core utilise désormais les types références Nullables (NRT) C#. Ainsi, vous obtenez les indications de compilateur appropriées pour l’utilisation de Null en cas d’utilisation de EF Core 6.0 à partir de votre propre code.

Microsoft.Data.Sqlite 6.0

Conseil

Vous pouvez exécuter et déboguer tous les exemples repris ci-dessous en téléchargeant l'exemple de code depuis GitHub.

Regroupement de connexions

Problème GitHub : #13837.

Il est courant de laisser les connexions de base de données ouvertes le moins de temps possible. Cela permet d’éviter la contention sur la ressource de connexion. C’est pourquoi les bibliothèques telles que EF Core ouvrent immédiatement la connexion avant d’effectuer une opération de base de données, puis la referment immédiatement après. Par exemple, prenons le code EF Core suivant :

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ée, est :

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.

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 instances DbConnection en fonction des besoins.

SQLite est un peu différent, car l’accès à la base de données implique généralement simplement l’accès à un fichier. Cela signifie que l’ouverture d’une connexion à une base de données SQLite est généralement très rapide. Ce n’est toutefois 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 de TimeOnly

Problème GitHub : #24506.

Microsoft.Data.Sqlite 6.0 prend en charge les nouveaux types DateOnly et TimeOnly de .NET 6. Ils 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 selon l’un des quatre types pris en charge. Microsoft.Data.Sqlite les stocke sous la forme TEXT. Par exemple, une entité qui utilise 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; }
}

Est mappée sur la table suivante de 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 manière normale. Par exemple, cette requête LINQ EF Core :

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

Se traduit comme suit dans 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 des anniversaires avant 1900 :

Found 'ajcvickers'
Found 'wendy'

API de points d’enregistrement

Problème GitHub : #20228.

Nous sommes en train de normaliser une API commune pour les points d’enregistrement dans les fournisseurs ADO.NET. 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 toute 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
  • Restaure le précédent point de sauvegarde 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();

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

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

Problème GitHub : #22505. @nmichels a contribué à cette fonctionnalité. Merci !

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

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

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

Conseil

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