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é
Modifications à fort impact
Les Contains
dans les requêtes LINQ peuvent cesser de fonctionner sur des versions antérieures de SQL Server
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
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
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
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
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
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 etIProperty.DeclaringType
doit être utilisé à la place.IEntityTypeIgnoredConvention
est désormais obsolète etITypeIgnoredConvention
doit être utilisé à la place.IValueGeneratorSelector.Select
accepte maintenant unITypeBase
qui peut être unIEntityType
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é
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
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
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é
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
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
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 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
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);
});
}
Les appels multiples à AddDbContext sont appliqués dans un ordre différent
Ancien comportement
Auparavant, lorsque plusieurs appels à AddDbContext
, AddDbContextPool
, AddDbContextFactory
ou AddPooledDbContextFactor
étaient effectués avec le même type de contexte mais avec une configuration conflictuelle, le premier avait la priorité.
Nouveau comportement
À partir d’EF Core 8.0, la configuration du dernier appel aura la priorité.
Pourquoi
Ce changement a été fait pour être cohérent avec la nouvelle méthode ConfigureDbContext
, qui peut être utilisée pour ajouter des configurations soit avant soit après les méthodes Add*
.
Corrections
Inversez l’ordre des appels Add*
.
EntityTypeAttributeConventionBase remplacé par TypeAttributeConventionBase
Nouveau comportement
Dans EF Core 8.0, EntityTypeAttributeConventionBase
a été renommé en TypeAttributeConventionBase
.
Pourquoi
TypeAttributeConventionBase
représente mieux la fonctionnalité, car il peut maintenant être utilisé pour des types complexes et des types d’entités.
Corrections
Remplacez les usages de EntityTypeAttributeConventionBase
par TypeAttributeConventionBase
.