Changements cassants dans EF Core 8 (EF8)

Cette page documente les changements de comportement et d’API qui peuvent casser les applications existantes qui mettent à jour EF Core 7 vers EF Core 8. Veillez à passer en revue les changements cassants antérieurs si vous effectuez une mise à jour à partir d’une version antérieure d’EF Core :

Framework cible

EF Core 8 cible .NET 8. Les applications ciblant les versions antérieures de .NET, .NET Core et .NET Framework devront mettre à jour pour cibler .NET 8.

Résumé

Modification critique Impact
Contains dans les requêtes LINQ peuvent cesser de fonctionner sur des versions antérieures de SQL Server Élevée
Les énumérations dans JSON sont stockées sous forme de ints au lieu de chaînes par défaut Élevée
SQL Serverdate et time génèrent maintenant des modèles automatiques vers .NET DateOnly et TimeOnly Moyenne
Les colonnes booléennes avec une valeur générée par une base de données ne sont plus générées automatiquement comme pouvant accepter la valeur Null Moyenne
Les méthodes SQLite Math se traduisent désormais en SQL Faible
ITypeBase remplace IEntityType dans certaines API Faible
Les expressions ValueGenerator doivent utiliser des API publiques Faible
ExcludeFromMigrations n’exclut plus les autres tables d’une hiérarchie TPC Faible
Les clés entières autres que l’ombre sont conservées dans des documents Cosmos Faible
Le modèle relationnel est généré dans le modèle compilé Bas
La génération automatique de modèles peut générer différents noms de navigation Bas
Les discriminateurs ont maintenant une longueur maximale Bas
Les valeurs de clé SQL Server sont comparées sans respect de la casse Bas

Modifications à fort impact

Les Contains dans les requêtes LINQ peuvent cesser de fonctionner sur des versions antérieures de SQL Server

Suivi de problème no 13617

Ancien comportement

Auparavant, lorsque l’opérateur Contains était utilisé dans les requêtes LINQ avec une liste de valeurs paramétrisées, EF générait du SQL qui était inefficace mais fonctionnait sur toutes les versions de SQL Server.

Nouveau comportement

À compter d’EF Core 8.0, EF génère désormais du SQL plus efficace, mais n’est pas pris en charge sur SQL Server 2014 et versions antérieures.

Notez que les versions plus récentes de SQL Server peuvent être configurées avec un niveau de compatibilité plus ancien, ce qui les rend également incompatibles avec le nouveau SQL. Cela peut également se produire avec une base de données Azure SQL qui a été migrée à partir d’une instance SQL Server locale précédente, portant l’ancien niveau de compatibilité.

Pourquoi

Le SQL précédent généré par EF Core pour Contains insérait les valeurs paramétrisées en tant que constantes dans le SQL. Par exemple, la requête LINQ suivante :

var names = new[] { "Blog1", "Blog2" };

var blogs = await context.Blogs
    .Where(b => names.Contains(b.Name))
    .ToArrayAsync();

... serait traduite en SQL comme suit :

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')

Une telle insertion de valeurs constantes dans le SQL crée de nombreux problèmes de performances, ce qui élimine la mise en cache du plan de requête et provoque des évictions inutiles d’autres requêtes. La nouvelle traduction EF Core 8.0 utilise la fonction SQL Server OPENJSON pour transférer plutôt les valeurs en tant que tableau JSON. Cela résout les problèmes de performances inhérents à la technique précédente ; toutefois, la fonction OPENJSON n’est pas disponible dans SQL Server 2014 et version antérieures.

Pour plus d’informations sur ce changement, consultez ce billet de blog.

Corrections

Si votre base de données est SQL Server 2016 (13.x) ou ultérieure, ou si vous utilisez Azure SQL, vérifiez le niveau de compatibilité configuré de votre base de données via la commande suivante :

SELECT name, compatibility_level FROM sys.databases;

Si le niveau de compatibilité est inférieur à 130 (SQL Server 2016), envisagez de le modifier en une valeur plus récente (documentation).

Sinon, si votre version de base de données est vraiment antérieure à SQL Server 2016 ou est définie sur un ancien niveau de compatibilité que vous ne pouvez pas modifier pour une raison quelconque, configurez EF Core pour revenir à l’ancien SQL moins efficace comme suit :

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));

Les énumérations dans JSON sont stockées sous forme de ints au lieu de chaînes par défaut

Suivi de problème no 13617

Ancien comportement

Dans EF7, les énumérations mappées au format JSON sont, par défaut, stockées sous forme de valeurs de chaîne dans le document JSON.

Nouveau comportement

À compter d’EF Core 8.0, EF mappe désormais, par défaut, des énumérations à des valeurs entières dans le document JSON.

Pourquoi

EF a toujours, par défaut, mappé des énumérations à une colonne numérique dans des bases de données relationnelles. Étant donné qu’EF prend en charge des requêtes où les valeurs de JSON interagissent avec des valeurs des colonnes et des paramètres, il est important que les valeurs dans JSON correspondent aux valeurs de la colonne non JSON.

Corrections

Pour continuer à utiliser des chaînes, configurez la propriété enum avec une conversion. Par exemple :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}

Ou pour toutes les propriétés du type d’énumération ::

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}

Changements à impact moyen

SQL Server date et time génèrent maintenant des modèles automatiques vers .NET DateOnly et TimeOnly

Suivi du problème nº24507

Ancien comportement

Précédemment, lors de la génération de modèles automatiques d’une base de données SQL Server avec des colonnes date ou time, EF générait des propriétés d’entité avec des types DateTime et TimeSpan.

Nouveau comportement

À compter d’EF Core 8.0, date et time sont automatiquement générés en tant que DateOnly et TimeOnly.

Pourquoi

DateOnly et TimeOnly ont été introduits dans .NET 6.0 et sont parfaitement compatibles pour le mappage des types d’heure et de date des bases de données. DateTime contient notamment un composant d’heure inutilisé qui peut être source de confusion lors de son mappage vers date et TimeSpan représente un intervalle de temps, incluant éventuellement des jours, plutôt qu’une heure de la journée à laquelle un événement se produit. L’utilisation de nouveaux types empêche les bogues et la confusion et offre une clarté de l’intention.

Corrections

Cette modification affecte uniquement les utilisateurs qui génèrent régulièrement des modèles de leur base de données dans un modèle de code EF (flux « base de données en premier »).

Il est recommandé de réagir à ce changement en modifiant votre code afin d’utiliser les nouveaux types de modèles automatiquesDateOnly et TimeOnly récemment générés. Toutefois, si cela n’est pas possible, vous pouvez modifier les modèles de génération de modèles automatique pour restaurer le mappage précédent. Pour ce faire, configurez les modèles tel que décrit sur cette page. Enfin, modifiez le fichier EntityType.t4, recherchez les propriétés d’entité get générées (recherchez property.ClrType) et changez le code par ce qui suit :

        var clrType = property.GetColumnType() switch
        {
            "date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
            "date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
            "time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
            "time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
            _ => property.ClrType
        };

        usings.AddRange(code.GetRequiredUsings(clrType));

        var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
        var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
    public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#

Les colonnes booléennes avec une valeur générée par une base de données ne sont plus générées automatiquement comme pouvant accepter la valeur Null

Suivi du problème nº 15070

Ancien comportement

Auparavant, les colonnes bool non-nullables avec une contrainte par défaut de base de données étaient générées sous forme de propriétés bool? pouvant accepter la valeur Null.

Nouveau comportement

À compter d’EF Core 8.0, les colonnes bool non-nullables sont toujours générées en tant que propriétés non-nullables.

Pourquoi

Une propriété bool n’a pas sa valeur envoyée à la base de données si cette valeur est false, qui est la valeur CLR par défaut. Si la base de données a une valeur par défaut de true pour la colonne, même si la valeur de la propriété est false, la valeur dans la base de données devient true. Toutefois, dans EF8, la sentinelle utilisée pour déterminer si une propriété a une valeur peut être modifiée. Cette opération est effectuée automatiquement pour les propriétés bool avec une valeur générée par une base de données de true, ce qui signifie qu’il n’est plus nécessaire de générer automatiquement la structure des propriétés comme pouvant accepter la valeur Null.

Corrections

Cette modification affecte uniquement les utilisateurs qui génèrent régulièrement des modèles de leur base de données dans un modèle de code EF (flux « base de données en premier »).

Nous vous recommandons de réagir à ce changement en modifiant votre code afin d’utiliser la propriété booléenne non-nullable. Toutefois, si cela n’est pas possible, vous pouvez modifier les modèles de génération de modèles automatique pour restaurer le mappage précédent. Pour ce faire, configurez les modèles tel que décrit sur cette page. Enfin, modifiez le fichier EntityType.t4, recherchez les propriétés d’entité get générées (recherchez property.ClrType) et changez le code par ce qui suit :

#>
        var propertyClrType = property.ClrType != typeof(bool)
                              || (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
            ? property.ClrType
            : typeof(bool?);
#>
    public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#

Modifications à faible impact

Les méthodes SQLite Math se traduisent désormais en SQL

Suivi de problème no 18843

Ancien comportement

Auparavant, seules les méthodes Abs, Max, Min et Round sur Math étaient traduites en SQL. Tous les autres membres étaient évalués sur le client s’ils apparaissaient dans l’expression Select finale d’une requête.

Nouveau comportement

Dans EF Core 8.0, toutes les méthodes Math avec les fonctions mathématiques SQLite correspondantes sont traduites en SQL.

Ces fonctions mathématiques ont été activées dans la bibliothèque SQLite native que nous fournissons par défaut (par le biais de notre dépendance sur le package NuGet SQLitePCLRaw.bundle_e_sqlite3). Elles ont également été activées dans la bibliothèque fournie par SQLitePCLRaw.bundle_e_sqlcipher. Si vous utilisez l’une de ces bibliothèques, votre application ne doit pas être affectée par cette modification.

Toutefois, il est possible que les applications incluant la bibliothèque SQLite native par d’autres moyens n’activent pas les fonctions mathématiques. Dans ces cas, les méthodes Math sont traduites en SQL et ne rencontrent des erreurs aucune fonction de ce type lorsqu’elles sont exécutées.

Pourquoi

SQLite a ajouté des fonctions mathématiques intégrées dans la version 3.35.0. Même si elles sont désactivées par défaut, elles sont devenus suffisamment omniprésentes que nous avons décidé de fournir des traductions par défaut pour elles dans notre fournisseur EF Core SQLite.

Nous avons également collaboré avec Eric Sink sur le projet SQLitePCLRaw pour activer les fonctions mathématiques dans toutes les bibliothèques SQLite natives fournies dans le cadre de ce projet.

Corrections

Le moyen le plus simple de corriger les cassures est, si possible, d’activer la fonction mathématique dans la bibliothèque SQLite native en spécifiant l’option de compilation SQLITE_ENABLE_MATH_FUNCTIONS.

Si vous ne contrôlez pas la compilation de la bibliothèque native, vous pouvez également corriger les cassures en créant vous-même les fonctions à l’exécution à l’aide des API Microsoft.Data.Sqlite.

sqliteConnection
    .CreateFunction<double, double, double>(
        "pow",
        Math.Pow,
        isDeterministic: true);

Vous pouvez également forcer l’évaluation du client en fractionnant l’expression Select en deux parties séparées par AsEnumerable.

// Before
var query = dbContext.Cylinders
    .Select(
        c => new
        {
            Id = c.Id
            // May throw "no such function: pow"
            Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
        });

// After
var query = dbContext.Cylinders
    // Select the properties you'll need from the database
    .Select(
        c => new
        {
            c.Id,
            c.Radius,
            c.Height
        })
    // Switch to client-eval
    .AsEnumerable()
    // Select the final results
    .Select(
        c => new
        {
            Id = c.Id,
            Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
        });

ITypeBase remplace IEntityType dans certaines API

Suivi du problème nº 13947

Ancien comportement

Auparavant, tous les types structurels mappés étaient des types d’entités.

Nouveau comportement

Avec l’introduction de types complexes dans EF8, certaines API qui utilisaient précédemment un IEntityType utilisent maintenant ITypeBase afin que les API puissent être utilisées avec des types d’entité ou complexes. Cela inclut :

  • IProperty.DeclaringEntityType est désormais obsolète et IProperty.DeclaringType doit être utilisé à la place.
  • IEntityTypeIgnoredConvention est désormais obsolète et ITypeIgnoredConvention doit être utilisé à la place.
  • IValueGeneratorSelector.Select accepte maintenant un ITypeBase qui peut être un IEntityType sans l’être obligatoirement.

Pourquoi

Avec l’introduction de types complexes dans EF8, ces API peuvent être utilisées avec IEntityType ou IComplexType.

Corrections

Les anciennes API sont obsolètes, mais ne seront pas supprimées jusqu’à EF10. Le code doit être mis à jour pour utiliser les nouvelles API ASAP.

Les expressions ValueConverter et ValueComparer doivent utiliser des API publiques pour le modèle compilé

Suivi du problème nº 24896

Ancien comportement

Auparavant, les définitions ValueConverter et ValueComparer n’étaient pas incluses dans le modèle compilé et pouvaient donc contenir du code arbitraire.

Nouveau comportement

EF extrait désormais les expressions à partir des objets ValueConverter et ValueComparer et inclut ces C# dans le modèle compilé. Cela signifie que ces expressions doivent uniquement utiliser une API publique.

Pourquoi

L’équipe EF déplace progressivement d’autres constructions dans le modèle compilé pour prendre en charge l’utilisation d’EF Core avec AOT à l’avenir.

Corrections

Rendez publiques les API utilisées par le comparateur. Par exemple, observez ce convertisseur simple :

public class MyValueConverter : ValueConverter<string, byte[]>
{
    public MyValueConverter()
        : base(v => ConvertToBytes(v), v => ConvertToString(v))
    {
    }

    private static string ConvertToString(byte[] bytes)
        => ""; // ... TODO: Conversion code

    private static byte[] ConvertToBytes(string chars)
        => Array.Empty<byte>(); // ... TODO: Conversion code
}

Pour utiliser ce convertisseur dans un modèle compilé avec EF8, les méthodes ConvertToString et ConvertToBytes doivent être rendues publiques. Par exemple :

public class MyValueConverter : ValueConverter<string, byte[]>
{
    public MyValueConverter()
        : base(v => ConvertToBytes(v), v => ConvertToString(v))
    {
    }

    public static string ConvertToString(byte[] bytes)
        => ""; // ... TODO: Conversion code

    public static byte[] ConvertToBytes(string chars)
        => Array.Empty<byte>(); // ... TODO: Conversion code
}

ExcludeFromMigrations n’exclut plus les autres tables d’une hiérarchie TPC

Suivi du problème nº 30079

Ancien comportement

Auparavant, l’utilisation de ExcludeFromMigrations sur une table dans une hiérarchie TPC excluait également d’autres tables de la hiérarchie.

Nouveau comportement

À compter d’EF Core 8.0, ExcludeFromMigrations n’affecte pas les autres tables.

Pourquoi

L’ancien comportement était un bogue et empêchait les migrations d’être utilisées pour gérer des hiérarchies entre plusieurs projets.

Corrections

Utilisez ExcludeFromMigrations explicitement sur toute autre table qui doit être exclue.

Les clés de type entier autres que l’ombre sont conservées dans des documents Cosmos

Suivi du problème nº 31664

Ancien comportement

Auparavant, les propriétés de type entier autres que l’ombre correspondant aux critères d’une propriété de clé synthétisée n’étaient pas conservées dans le document JSON, mais étaient synthétisées de nouveau en sortie.

Nouveau comportement

À compter d’EF Core 8.0, ces propriétés sont désormais conservées.

Pourquoi

L’ancien comportement était un bogue et empêchait les propriétés correspondant aux critères de clés synthétisées d’être conservées dans Cosmos.

Corrections

Excluez la propriété du modèle si sa valeur ne doit pas être conservée. En outre, vous pouvez désactiver ce comportement en définissant le commutateur AppContext Microsoft.EntityFrameworkCore.Issue31664 sur true. Pour plus d’informations, consultez la section AppContext pour les utilisateurs de bibliothèques.

AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);

Le modèle relationnel est généré dans le modèle compilé

Suivi du problème nº 24896

Ancien comportement

Auparavant, le modèle relationnel était calculé au moment de l’exécution, même en cas d’utilisation d’un modèle compilé.

Nouveau comportement

À compter d’EF Core 8.0, le modèle relationnel fait partie du modèle compilé généré. Toutefois, pour les modèles particulièrement volumineux, la compilation du fichier généré peut échouer.

Pourquoi

Ce comportement est prévu pour améliorer le temps de démarrage.

Corrections

Modifiez le fichier généré *ModelBuilder.cs et supprimez la ligne AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel()); ainsi que la méthode CreateRelationalModel().

La génération automatique de modèles peut générer différents noms de navigation

Suivi de problème n° 27832

Ancien comportement

Auparavant, pendant la génération automatique de modèles pour DbContext et les types d’entités à partir d’une base de données existante, les noms de navigation pour les relations étaient parfois dérivés d’un préfixe commun de plusieurs noms de colonne de clé étrangère.

Nouveau comportement

À compter d’EF Core 8.0, les préfixes communs des noms de colonne à partir d’une clé étrangère composite ne sont plus utilisés pour générer des noms de navigation.

Pourquoi

Il s’agit d’une règle de nommage obscure qui génère parfois des noms très médiocres comme S, Student_ ou même juste _. Sans cette règle, des noms étranges ne sont plus générés et les conventions de nommage pour les navigations sont également simplifiées, ce qui facilite la compréhension et la prédiction des noms générés.

Corrections

Les outils EF Core Power Tools ont la possibilité de continuer à générer des navigations en utilisant l’ancienne méthode. Le code généré peut également être entièrement personnalisé avec des modèles T4. Cela peut être utilisé pour illustrer les propriétés de clé étrangère des relations de génération automatique de modèles, et utiliser la règle appropriée pour votre code afin de générer les noms de navigation dont vous avez besoin.

Les discriminateurs ont maintenant une longueur maximale

Suivi de problème n° 10691

Ancien comportement

Auparavant, les colonnes de discrimination créées pour le mappage d’héritage TPH étaient configurées au format nvarchar(max) sur SQL Server/Azure SQL, ou sous forme de type de chaîne non lié équivalent sur d’autres bases de données.

Nouveau comportement

À compter d’EF Core 8.0, les colonnes de discrimination sont créées avec une longueur maximale qui couvre toutes les valeurs de discrimination connues. EF génère une migration pour faire ce changement. Toutefois, si la colonne de discrimination est limitée d’une façon ou d’une autre, par exemple, dans le cadre d’un index, la colonne AlterColumn créée par les migrations peut échouer.

Pourquoi

Les colonnes nvarchar(max) sont inefficaces et inutiles quand les longueurs de toutes les valeurs possibles sont connues.

Corrections

La taille de colonne peut être rendue explicitement indépendante :

modelBuilder.Entity<Foo>()
    .Property<string>("Discriminator")
    .HasMaxLength(-1);

Les valeurs de clé SQL Server sont comparées sans respect de la casse

Suivi de problème n° 27526

Ancien comportement

Auparavant, pour le suivi des entités avec des clés de chaîne avec les fournisseurs de base de données SQL Server/Azure SQL, les valeurs de clé étaient comparées en utilisant le comparateur ordinal sensible à la casse .NET par défaut.

Nouveau comportement

À compter d’EF Core 8.0, les valeurs de clé de chaîne SQL Server/Azure SQL sont comparées avec le comparateur ordinal insensible à la casse .NET par défaut.

Pourquoi

Par défaut, SQL Server utilise des comparaisons insensibles à la casse pendant la comparaison des valeurs de clé étrangère pour rechercher les correspondances par rapport aux valeurs de clé principale. Cela signifie que quand EF utilise des comparaisons sensibles à la casse, il peut ne pas connecter une clé étrangère à une clé principale quand il le doit.

Corrections

Les comparaisons sensibles à la casse peuvent être utilisées en définissant un ValueComparer personnalisé. Par exemple :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.Ordinal),
        v => v.GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .Metadata.SetValueComparer(comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
            b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
        });
}