Nouveautés de EF Core 6,0

EF Core 6,0 a atteint la qualité release candidate. Cette page contient une vue d’ensemble des changements intéressants introduits dans cette version.

Conseil

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

SQL Server les tables temporelles

GitHub Problème : #4693.

SQL Server tables temporelles 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 d’historique horodatées sont stockées chaque fois qu’une modification est apportée à la table principale. Cela permet l’interrogation des données d’historique, telles que l’audit ou la 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 d’historique
  • Restauration des 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());

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

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

La 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 masquées datetime2 appelé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 Shadow 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 l’heure UTC générée par SQL Server. Les heures UTC sont utilisées pour toutes les opérations impliquant des tables temporelles, telles que dans les requêtes indiquées ci-dessous.

Notez également qu’une table d’historique associée appelée EmployeeHistory est créée automatiquement. Les noms des colonnes de période et de la table d’historique peuvent être modifiés avec une configuration supplémentaire 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 d’historique 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 la 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 normale. Par exemple :

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

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

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

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

Cela imprime :

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

Notez que la ValidTo colonne (appelée par défaut PeriodEnd ) contient la datetime2 valeur max. C’est toujours le cas pour les lignes actuelles dans la table. Les ValidFrom colonnes (appelées par défaut PeriodStart ) contiennent l’heure UTC à laquelle la ligne a été insérée.

Interrogation des données d’historique

EF Core prend en charge l’interrogation des données d’historique de la table par le biais de 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 d’historique pour une clé primaire donnée.
  • TemporalAll: Retourne toutes les lignes dans les données d’historique. Il s’agit généralement de nombreuses lignes de la table d’historique 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 pour une clé primaire donnée.
  • TemporalBetween: Identique à TemporalFromTo , sauf que les lignes sont incluses qui sont devenues actives sur la limite supérieure.
  • TemporalContainedIn:: Retourne toutes les lignes qui ont commencé à être actives et qui sont terminées pendant deux heures UTC données. Il peut s’agir de nombreuses lignes de la table d’historique pour une clé primaire donnée.

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

Par exemple, après avoir apporté des mises à jour et des suppressions à nos données, nous pouvons exécuter une requête à l’aide TemporalAll de pour afficher les données d’historique :

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

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

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

Cette requête renvoie les données suivantes :

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

Notez que la dernière ligne retournée a cessé d’être active à 8/26/2021 4:44:59 h 00. Cela est dû au fait que la ligne du tiret arc-en-ciel 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 TemporalFromTo de, TemporalBetween ou 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 d’historique

Comme indiqué ci-dessus, le tiret arc-en-ciel a été supprimé de la Employees table. Il s’agit 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 le tiret arc-en-ciel telle qu’elle était à l’heure UTC spécifié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 il 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 arc-en-ciel, l’interrogation des données d’historique 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

Lots de migration

GitHub Problème : #19693.

EF Core migrations 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 de schéma doivent être appliquées au moment du déploiement de l’application, souvent dans le cadre d’un système d’intégration continue/de déploiement continu (C.I./C.D.).

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

Notes

pour plus d’informations sur les migrations, les offres groupées et le déploiement, consultez DevOps présentation des offres de migration EF Core conviviales sur le Blog .net.

Les lots de migration sont créés à l’aide de l' dotnet ef outil en ligne de commande. Avant de continuer, vérifiez que vous avez installé la dernière version de l’outil .

Un bundle nécessite des migrations pour inclure. Celles-ci sont créées à l’aide de dotnet ef migrations add , comme décrit dans la documentation sur les migrations. Une fois que vous avez des migrations prêtes à être déployées, créez un bundle à l’aide du dotnet ef migrations 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 fichier exécutable adapté à votre système d’exploitation cible. dans mon cas, il s’agit de Windows x64. j’obtiens donc une efbundle.exe suppression dans mon dossier local. L’exécution de cet exécutable applique les migrations qu’il contient :

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

Les migrations sont appliquées à la base de données uniquement si elles n’ont pas déjà été appliquées. Par exemple, l’exécution du même Bundle ne fait rien, puisqu’il n’y a pas de nouvelles migrations à appliquer :

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

Toutefois, si des modifications sont apportées au modèle et que d’autres migrations sont générées avec dotnet ef migrations add , elles peuvent être regroupées dans un nouveau fichier exécutable prêt à être appliqué. 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' --force option peut être utilisée pour remplacer le bundle existant par un nouveau.

L’exécution de cette nouvelle offre groupée applique ces deux nouvelles migrations à la base de données :

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

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

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

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

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

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

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

Notez que, par défaut, chaque migration est appliquée dans sa propre transaction. pour plus d’informations sur les futures améliorations possibles dans ce domaine, consultez GitHub #22616 de problème .

Configuration du modèle de préconvention

GitHub Problème : #12229.

Les versions antérieures de EF Core requièrent que le mappage de chaque propriété d’un type donné soit explicitement configuré lorsque ce mappage diffère de la valeur par défaut. Cela comprend les « facettes », telles que la longueur maximale des chaînes et la précision des décimales, ainsi que la conversion des valeurs pour le type de propriété.

Cela a nécessité :

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

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

EF Core 6,0 autorise la spécification de cette configuration de mappage une seule fois pour un type donné. Elle sera ensuite appliquée à toutes les propriétés de ce type dans le modèle. C’est ce qu’on appelle la « configuration de modèle de préconvention », car elle configure les aspects du modèle qui sont ensuite utilisés par les conventions de construction du modèle. Une telle configuration est appliquée en remplaçant ConfigureConventions sur votre DbContext :

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

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

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

    public Session CurrentSession { get; set; }

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

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

    public Customer Customer { get; set; }
}

Toutes les propriétés de chaîne peuvent être configurées pour être ANSI (au lieu 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 DateTime en longs :

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

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

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

En supposant 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 lorsque vous travaillez avec 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,
    PoundsStirling
}

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 JSON sérialisé est stocké. Dans ce cas, la colonne est limitée à une longueur maximale de 64.

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

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

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

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

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

Notes

Pour plus d’informations et des exemples de configuration de modèle de préconvention, consultez annonce de Entity Framework Core 6,0 Preview 6 : configure conventions sur le blog .net.

Modèles compilés

GitHub Problème : #1906.

Les modèles compilés peuvent améliorer le temps de démarrage EF Core pour les applications avec des modèles volumineux. Un modèle de grande taille signifie généralement entre centaines et 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 DbContext lorsque ce type DbContext est utilisé pour la première fois dans l’application. Notez que la simple 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 typiques qui provoquent l’initialisation du modèle incluent DbContext.Add l’appel ou l’exécution de la première requête.

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

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

dotnet ef dbcontext optimize

Les --output-dir --namespace options et 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 comprend un morceau de code pour copier-coller dans votre configuration DbContext afin de faire en sorte 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.

En outre, plusieurs modèles compilés peuvent être générés pour les types DbContext qui peuvent utiliser différents modèles en fonction de la configuration de l’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 votre EF Core est trop lent. La compilation de petits modèles n’en est généralement pas la mérite.

Si la prise en charge de ces fonctionnalités est essentielle pour votre réussite, veuillez voter pour les problèmes appropriés liés.

Benchmarks

Conseil

Vous pouvez essayer de compiler un modèle volumineux et d’y exécuter un test en téléchargeant l’exemple de code à partir de GitHub.

le modèle dans le GitHub référentiel référencé ci-dessus contient 449 types d’entités, propriétés 6390 et 720 relations. Il s’agit d’un modèle modérément grand. En utilisant BenchmarkDotNet pour mesurer, la durée moyenne de la première requête est de 1,02 secondes 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.

ajouter une Base de données locale de connexion

Notes

Pour plus d’informations sur les modèles compilés et les performances de démarrage EF Core, consultez annonce de Entity Framework Core 6,0 Preview 5 : modèles compilés sur le blog .net.

Amélioration des performances sur les TechEmpower fortune

GitHub Problème : #23611.

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

  • Les performances de EF Core 6,0 sont à présent de 70% plus rapides sur le banc d’essai standard du classement Fortune TechEmpower, par rapport à 5,0.
    • Il s’agit de l’amélioration des performances de la pile complète, y compris les améliorations apportées au code d’évaluation, au Runtime .NET, etc.
  • EF Core 6,0 est en fait 31% d’exécution plus rapide des requêtes non suivies.
  • 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 le EF Core dans l’évaluation de la TechEmpower du classement Fortune est limité de 55% à environ 5%.

Notes

Pour obtenir une présentation détaillée des améliorations des performances des requêtes dans EF Core 6,0, consultez annonce de Entity Framework Core 6,0 Preview 4 : performance Edition sur le blog .net.

améliorations du fournisseur Cosmos

EF Core 6,0 contient de nombreuses améliorations pour le fournisseur de base de données Azure Cosmos DB.

Conseil

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

Propriété implicite par défaut

GitHub Problème : #24803.

lors de la génération d’un modèle pour le fournisseur Cosmos, EF Core 6,0 marque par défaut les types d’entités enfants appartenant à leur entité parente. cela élimine le besoin de la plupart OwnsMany des OwnsOne appels et dans le modèle Cosmos. 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 Cosmos avec la configuration suivante :

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

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

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

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

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

les documents de Cosmos résultants ont les parents, les enfants, les animaux et les adresses 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
}

Notes

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

Collections de types primitifs

GitHub Problème : #14762.

EF Core 6,0 mappe en mode natif des collections de types primitifs lors de l’utilisation du fournisseur de base de données Cosmos. Par exemple, considérez ce 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 tous les deux être remplis et insérés dans la base de données de la manière habituelle :

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 génère 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 regroupements peuvent ensuite être mis à jour, 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 dotés de clés de type chaîne sont pris en charge
  • L’interrogation du contenu des collections primitives n’est pas prise en charge actuellement. Votez pour #16926, #25700et #25701 si ces fonctionnalités sont importantes pour vous.

Traductions en fonctions intégrées

GitHub Problème : #16143.

le fournisseur Cosmos traduit maintenant d’autres méthodes BCL (Base Class Library) pour Cosmos fonctions intégrées. Les tableaux suivants affichent les traductions qui sont nouvelles dans EF Core 6,0.

Traductions de chaînes

BCL, méthode 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
+ and CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL Appels ne respectant pas la casse uniquement

Les traductions pour LOWER , LTRIM ,,, RTRIM TRIM UPPER et SUBSTRING ont été fournies par @Marusyk . Merci !

Par exemple :

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

Ce qui se traduit par :

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

Traductions mathématiques

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

Ces traductions ont été fournies par @Marusyk . Merci !

Par exemple :

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

Ce qui se traduit par :

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

Traductions DateTime

BCL, méthode Fonction intégrée
DateTime.UtcNow GetCurrentDateTime

Ces traductions ont été fournies par @Marusyk . Merci !

Par exemple :

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

Ce qui se traduit par :

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

requêtes de SQL brutes avec FromSql

GitHub Problème : #17311.

il est parfois nécessaire d’exécuter une requête de SQL brute au lieu d’utiliser LINQ. cela est désormais pris en charge avec le fournisseur Cosmos via l’utilisation de la FromSql méthode. Cela fonctionne de la même manière qu’avec les fournisseurs relationnels. Par exemple :

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

Qui est exécutée en tant que :

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

Requêtes distinctes

GitHub Problème : #16144.

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

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

Ce qui se traduit par :

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

Diagnostics

GitHub Problème : #17298.

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

Notes

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

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

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

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 de la base de données Cosmos à l’aide de Find avec une clé de partition génère les CosmosEventId.ExecutingReadItem CosmosEventId.ExecutedReadItem événements et. Par exemple, le code suivant :

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 Cosmos génère l' CosmosEventId.ExecutedReplaceItem événement. Par exemple, le code suivant :

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 Cosmos génère l' CosmosEventId.ExecutedDeleteItem événement. Par exemple, le code suivant :

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

GitHub Problème : #17301.

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

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

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.HasAutoscaleThroughput(3000);
        entityTypeBuilder.HasAutoscaleThroughput(12000);
    });

Configurer la durée de vie

GitHub Problème : #17307.

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

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

Résoudre la fabrique de clients HTTP

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

le HttpClientFactory utilisé par le fournisseur de Cosmos peut désormais être défini explicitement. cela peut être particulièrement utile lors des tests, par exemple pour contourner la validation de certificat lors de l’utilisation de l’émulateur de Cosmos sur Linux :

Notes

pour obtenir un exemple détaillé de l’application des améliorations du fournisseur de Cosmos à une application existante, consultez la page utilisation du fournisseur EF Core Azure Cosmos DB pour un lecteur d’évaluation 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 inverse d’un modèle EF à partir d’une base de données existante.

Génération de modèles automatique de relations plusieurs-à-plusieurs

GitHub Problème : #22475.

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

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

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([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 [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE);

Ces tables peuvent être structurées à partir de la ligne de commande. Par exemple :

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

Cela donne lieu à une classe pour la publication :

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 la balise :

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

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

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

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

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

Types de référence Nullable C# de l’échafaudage

GitHub Problème : #15520.

EF Core 6,0 génère à présent un modèle EF et des types d’entités qui utilisent des types de référence Nullable C# (NRTs). L’utilisation de diagnostics proactifs NRT est automatiquement structurée lorsque la prise en charge de diagnostics proactifs NRT est activée dans le projet C# dans lequel le code est généré en structure.

Par exemple, le tableau suivant contient à la Tags fois des colonnes de chaîne qui n’autorisent pas 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, les Posts tables suivantes contiennent une relation obligatoire avec la Blogs table :

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

Cela entraîne la génération 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 DbContext généré sont créées de manière Diagnostics proactifs 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 au format de commentaires de code

GitHub Problème : #19113. Cette fonctionnalité a été fournie par @ErikEJ . 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 de 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 de requêtes LINQ.

Amélioration de la prise en charge de GroupBy

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

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

  • Translater 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' GroupBy application de l’opérateur

Voici des exemples de requêtes des rapports des clients 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]

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

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

À compter de EF Core 6,0, les appels à String.Concat avec plusieurs arguments sont désormais traduits en SQL. Par exemple, la requête suivante :

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

sera traduite dans le SQL suivant lors de l’utilisation de SQL Server :

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

Intégration plus lisse avec System. Linq. Async

GitHub Problème : #24041.

Le package System. Linq. Async ajoute un traitement LINQ asynchrone côté client. L’utilisation de ce package avec les versions précédentes de EF Core était lourde 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 mise en correspondance des modèles C# pour IAsyncEnumerable<T> que la EF Core exposée DbSet<TEntity> n’ait pas besoin d’implémenter l’interface directement.

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

GitHub Problème : #23921.

Dans EF Core 6,0, nous avons assoupli les exigences de paramètres pour 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 valeur. Par exemple, considérez un type d’entité avec une Name propriété 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 maintenant être exécutée à l’aide Contains de ou FreeText même si le type de la propriété n’est Name pas string . Par exemple :

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

cela génère le 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

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

les appels à ToString() sont désormais traduits en SQL lors de l’utilisation du fournisseur de base de données SQLite. Cela peut être utile pour les recherches de texte impliquant des colonnes qui ne sont pas des chaînes. Par exemple, considérez un User type d’entité 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 nombre en une chaîne dans la base de données. Nous pouvons ensuite utiliser cette chaîne avec une fonction telle que LIKE pour rechercher des nombres qui correspondent à un modèle. Par exemple, pour rechercher tous les nombres contenant 555 :

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

cela se traduit par les SQL suivants lors de l’utilisation d’une base de données SQLite :

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

notez que la traduction de ToString() 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 bases de données.

EF. Functions. Random

GitHub Problème : #16141. Cette fonctionnalité a été fournie par @RaymondHuy . 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 EF Core référentiel pour SQL Server, SQLite et Cosmos. Par exemple, considérez un User type d’entité avec une Popularity propriété :

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

Popularity peut avoir des valeurs comprises entre 1 et 5 inclus. À l’aide 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() * 5.0) + 1).ToList();

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

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

traduction de SQL Server améliorée pour IsNullOrWhitespace

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

Considérez la requête suivante :

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

Avant le EF Core 6,0, le code suivant a été converti en 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 à :

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 de la requête pour le fournisseur en mémoire

GitHub Problème : #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 lorsque ces vues retournent des types d’entité de clé inférieure. Par exemple, considérez une base de données client pour les clients basés sur le Royaume-Uni. Chaque client a une adresse :

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

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

Imaginez maintenant que nous souhaitons 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 ceci :

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

Et définissent une propriété DbSet pour celle-ci sur DbContext, ainsi que des jeux 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()
                        }));
}

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

var results = context.CustomerDensities.ToList();

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

GitHub Problème : #20173. Cette fonctionnalité a été fournie par @stevendarby . 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");

cela se traduit par le SQL suivant lors de l’utilisation de SQL Server :

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

Fractionner les requêtes pour les collections non de navigation

GitHub Problème : #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ù les 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 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 de 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 de 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 de 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 de 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 de 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 de 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

GitHub Problème : #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 EF pour générer les regroupements nécessaires et peut avoir un impact sur les performances. Par conséquent, EF Core 6,0 cette clause est supprimée.

Par exemple, envisagez la requête suivante :

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

avec EF Core 5,0 sur SQL Server, cette requête est traduite comme suit :

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

Avec EF Core 6,0, elle est traduite en :

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

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

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

Les balises de requête permettent d’ajouter une balise texturée à une requête LINQ de sorte qu’elle soit ensuite incluse dans le 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 entraîne l’SQL générée suivante lors de l’utilisation de SQL Server :

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

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

Modifications apportées à la gestion dépendante facultative

GitHub Problème : #24558.

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

Par exemple, imaginons une Customer classe où chaque client a un propriétaire Address :

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

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

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

L’adresse est facultative, ce qui signifie qu’elle est valide pour enregistrer un client sans adresse :

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

Toutefois, si un client a une adresse, cette adresse doit avoir au moins un code postal non NULL :

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

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

Désormais, lorsque les clients sont interrogés, si la colonne code postal a la valeur null, cela signifie que le client n’a pas d’adresse et que la Customer.Address propriété de navigation a la valeur null. Par exemple, en effectuant une itération au sein des clients et en vérifiant si l’adresse est NULL :

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

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

Génère les résultats suivants :

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

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 à la fois un client sans adresse et un client avec une adresse où toutes les propriétés d’adresse sont NULL :

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

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

Toutefois, dans la base de données, ces deux cas ne peuvent pas être distingués, comme nous pouvons le voir en interrogeant directement les colonnes de base de données :

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

C’est la raison pour laquelle EF Core 6,0 vous avertit lors de l’enregistrement d’un dépendant facultatif où toutes ses propriétés ont la valeur null. Par exemple :

Warn : 9/27/2021 09:25:01.338 RelationalEventId. OptionalDependentWithAllNullPropertiesWarning [20704] (Microsoft. EntityFrameworkCore. Update) l’entité de type’address’avec les valeurs de clé primaire {CustomerId :-2147482646} est un dépendant facultatif à l’aide du partage de table. L’entité n’a aucune propriété avec une valeur non définie par défaut pour déterminer si l’entité existe. Cela signifie que lorsqu’il est interrogé, aucune instance d’objet n’est créée à la place d’une instance avec toutes les propriétés définies sur les valeurs par défaut. Les dépendants imbriqués seront également perdus. N’enregistrez pas d’instance avec uniquement des valeurs par défaut ou marquez la navigation entrante comme requis dans le modèle.

Cela devient encore plus délicat lorsque le dépendant facultatif lui-même agit un principal pour un autre dépendant facultatif, également mappé à la même table.

La ligne du bas ici consiste à éviter le cas où un dépendant facultatif peut contenir toutes les valeurs de propriété Nullable et partage une table avec son principal. Il existe trois moyens faciles d’éviter ce qui suit :

  1. Rendez le dépendant obligatoire. Cela signifie que l’entité dépendante aura toujours une valeur une fois qu’elle sera interrogée, même si toutes ses propriétés ont la valeur null.
  2. Assurez-vous que le dépendant contient au moins une propriété obligatoire, comme décrit ci-dessus.
  3. Enregistrez les dépendants facultatifs dans leur propre table, au lieu de partager une table avec le principal.

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

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

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

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

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

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

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

pour plus d’exemples de dépendants facultatifs, consultez OptionalDependentsSample dans GitHub, y compris les cas avec des dépendants facultatifs imbriqués.

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

GitHub Problème : #19794. Cette fonctionnalité a été fournie par @RaymondHuy . 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 Book type d’entité avec une propriété pour le numéro ISBN (International Standard Book Number) au format « 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 ISBNs ne peuvent pas contenir de caractères non-Unicode, l' Unicode attribut 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 aboutit à 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]));

Notes

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

PrecisionAttribute

GitHub Problème : #17914. Cette fonctionnalité a été fournie par @RaymondHuy . 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 Product type d’entité 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 mappera 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

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

IEntityTypeConfiguration<TEntity> les instances permettent ModelBuilder à la configuration de chaque type d’entité d’être contenue 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 dans à 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 EntityTypeConfigurationAttribute peut être placé sur le type d’entité afin que EF Core puisse trouver 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 spécifiée IEntityTypeConfiguration chaque fois que le Book type d’entité est inclus dans un modèle. Le type d’entité est inclus dans un modèle à l’aide de l’un des mécanismes normaux. Par exemple, en créant une DbSet<TEntity> propriété 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>();
}

Notes

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

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

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

prise en charge des colonnes éparses SQL Server

GitHub Problème : #8023.

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

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

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

Il peut y avoir des millions d’utilisateurs, avec seulement quelques modérateurs. Cela signifie que le mappage de ForumName As Sparse peut être pertinent ici. Cela peut maintenant être configuré à l’aide IsSparse de dans OnModelCreating . Par exemple :

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

EF Core migrations marque 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]));

Notes

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 bon choix pour votre scénario.

Améliorations apportées à l’API HasConversion

GitHub Problème : #25468.

Avant le EF Core 6,0, les surcharges génériques des HasConversion méthodes utilisaient le paramètre générique pour spécifier le type vers lequel effectuer la conversion. Prenons l’exemple d’une Currency énumération :

public enum Currency
{
    UsDollars,
    PoundsStirling,
    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 à la place spécifier un type de convertisseur de valeur. 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 valeur 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.PoundsStirling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsStirling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

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

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

Configuration moins importante pour les relations plusieurs-à-plusieurs

GitHub Problème : #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 une entité partagée typée basée 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é de manière complémentaire 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

GitHub Problème : #13850.

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

À partir de EF Core 6,0, vous pouvez créer un convertisseur de valeur qui convertit les valeurs NULL. Cela peut être utile, par exemple, lorsque la base de données contient des valeurs NULL, mais que le type d’entité veut utiliser une autre valeur par défaut pour la propriété. Par exemple, Imaginez une énumération où sa valeur par défaut est « inconnu » :

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese 
}

Toutefois, la base de données peut avoir des valeurs NULL lorsque la race est inconnue. Dans EF Core 6,0, un convertisseur de valeurs peut être utilisé pour prendre en compte ce qui suit :

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

Les chats dont la race est « inconnu » verront leur Breed colonne avec 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();

Qui génère les instructions INSERT suivantes sur SQL Server :

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

Améliorations de la fabrique DbContext

AddDbContextFactory inscrit également DbContext directement

GitHub Problème : #25164.

Parfois, il est utile d’avoir à la fois un type DbContext et une fabrique pour les contextes de ce type, tous deux enregistrés dans le conteneur d’injection de dépendances d’applications (D.I.). Cela permet, par exemple, qu’une instance délimitée de DbContext soit résolue à 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 cela en charge, AddDbContextFactory inscrit également le type DbContext en tant que service étendu. Par exemple, considérez cette inscription dans le D.I. de l’application container :

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

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

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

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

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

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

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

À un niveau supérieur, cela signifie que la DbContext de la fabrique peut être injectée dans d’autres D.I. types de données. 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

GitHub Problème : #24124.

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

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

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

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

GitHub Problème : #24137.

Le PooledDbContextFactory type a été rendu public afin qu’il puisse être utilisé comme un pool autonome pour les instances de 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")
    .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 lorsqu’elles sont supprimées.

Améliorations diverses

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

API EF Core minimale

GitHub Problème : #25192.

.NET Core 6,0 comprend des modèles mis à jour qui présentent des « API minimales » simplifiées qui suppriment un grand nombre 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==");

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

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

Notes

Les EF Core les API minimales prennent uniquement en charge l’inscription et la configuration très basiques d’un 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 ces ressources pour en savoir plus sur les API minimales :

Conserver le contexte de synchronisation dans SaveChangesAsync

GitHub Problème : #23971.

Nous avons modifié le code EF Core dans la version 5,0 afin de lui affecter la valeur Task.ConfigureAwait à false tous les endroits où nous avons du await code asynchrone. 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 dans les 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 s’exécuter sur le U.I. thread. Par conséquent, nous rétablissons cette modification dans EF Core 6,0 pour la SaveChangesAsync méthode uniquement.

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

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

La base de données en mémoire EF Core lèvera à présent 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 User type avec une Username propriété obligatoire :

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

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

Si vous tentez d’enregistrer une entité avec une valeur null Username , l’exception suivante se produit :

Microsoft. EntityFrameworkCore. exception 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

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

Le CommandEventData fourni aux sources et intercepteurs de diagnostic contient désormais une valeur d’énumération qui indique quelle partie d’EF était responsable de la création de la commande. Vous pouvez l’utiliser comme filtre dans les diagnostics ou l’intercepteur. Par exemple, nous pouvons souhaiter un intercepteur qui s’applique uniquement aux commandes provenant de SaveChanges :

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

        return result;
    }
}

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

Saving changes for CustomersContext:

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

Meilleure gestion des valeurs temporaires

GitHub Problème : #24245.

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

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

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

La Id propriété de clé obtient une valeur temporaire dès qu’un Blog est suivi 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, le code suivant :

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

Génère la sortie suivante :

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

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

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

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

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

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

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

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

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

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

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

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

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

Ce qui donne :

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

EF Core annoté pour les types de référence Nullable C#

GitHub Problème : #19007.

Le code base EF Core utilise désormais les types de référence Nullable C# (NRTs) dans tout. Cela signifie que vous obtiendrez les indications de compilateur appropriées pour l’utilisation de null lors de l’utilisation de EF Core 6,0 à partir de votre propre code.

Microsoft. Data. sqlite 6,0

Conseil

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

Regroupement de connexions

GitHub Problème : #13837.

Il est courant de maintenir 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 comme 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, considérez ce EF Core code :

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.

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

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

SQLite est un peu différent, car l’accès à la base de données est 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. Toutefois, ce n'est pas toujours le cas. Par exemple, l’ouverture d’une connexion à une base de données chiffrée peut être très lente. Par conséquent, les connexions SQLite sont désormais regroupées lors de l’utilisation de Microsoft. Data. sqlite 6,0.

Prendre en charge DateOnly et TimeOnly

GitHub Problème : #24506.

Microsoft. Data. sqlite 6,0 prend en charge les nouveaux DateOnly TimeOnly types et de .net 6. Elles peuvent également être utilisées dans EF Core 6,0 avec le fournisseur SQLite. Comme toujours avec SQLite, son système de type natif signifie que les valeurs de ces types doivent être stockées en tant qu’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; }
}

Cartes le tableau suivant 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 EF Core requête LINQ :

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

Est traduit en ce qui suit sur SQLite :

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

Et retourne uniquement les utilisations avec anniversaires avant 1900 CE :

Found 'ajcvickers'
Found 'wendy'

API d’enregistrement des points de enregistrement

GitHub Problème : #20228.

nous sommes en cours de normalisation sur une API commune pour les points d’enregistrement dans les fournisseurs de ADO.NET. Microsoft. Data. sqlite prend désormais en charge cette API, notamment :

L’utilisation d’un point de sauvegarde 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 point de sauvegarde précédent créé
  • Valide la transaction
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
connection.Open();

using var transaction = connection.BeginTransaction();

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

transaction.Save("MySavepoint");

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

transaction.Rollback("MySavepoint");

transaction.Commit();

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

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

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

les fournisseurs de ADO.NET prennent en charge deux délais d’attente distincts :

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

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

Conseil

SQLite traite Default Timeout comme synonyme de Command Timeout et peut donc être utilisé à la place si préféré.