Utiliser des types références nullables
C# 8 a introduit une nouvelle fonctionnalité appelée types de référence pouvant accepter la valeur Null (NRT), ce qui permet aux types de référence d’être annotés, indiquant s’ils peuvent contenir la valeur null
ou non. Si vous ne connaissez pas cette fonctionnalité, il est recommandé de vous familiariser avec elle en lisant la documentation C#. Les types de référence nullables sont activés par défaut dans les nouveaux modèles de projet, mais restent désactivés dans les projets existants, à moins que vous ne l'ayez explicitement choisi.
Cette page présente la prise en charge d’EF Core pour les types références nullables et décrit les meilleures pratiques pour les utiliser.
Propriétés obligatoires et facultatives
La documentation principale sur les propriétés obligatoires et facultatives, et leur interaction avec les types références nullables est la page Propriétés obligatoires et facultatives. Il est recommandé de commencer par lire cette page.
Remarque
Soyez prudent lors de l’activation des types références nullables sur un projet existant : les propriétés de type référence précédemment configurées comme facultatives sont désormais configurées comme obligatoires, sauf si elles sont explicitement annotées pour pouvant accepter la valeur Null. Lors de la gestion d’un schéma de base de données relationnelle, cela peut entraîner la génération de migrations qui modifient l’acceptation de la valeur Null de la colonne de la base de données.
Propriétés et initialisation non-nullables
Lorsque les types références nullables sont activés, le compilateur C# émet des avertissements pour toute propriété non-nullable non initialisée, car ceux-ci contiendraient null
. Par conséquent, cette méthode courante d’écriture des types d’entités ne peut pas être utilisée :
public class Customer
{
public int Id { get; set; }
// Generates CS8618, uninitialized non-nullable property:
public string Name { get; set; }
}
Si vous utilisez C# 11 ou version ultérieure, les membres requis fournissent la solution idéale à ce problème :
public required string Name { get; set; }
Le compilateur garantit maintenant que lorsque votre code instancie un client, il initialise toujours sa propriété Name. Et étant donné que la colonne de base de données mappée à la propriété est non-nullable, toutes les instances chargées par EF contiennent toujours également un nom non-null.
Si vous utilisez une version antérieure de C#, la liaison de constructeur est une autre technique pour vous assurer que vos propriétés non-nullables sont initialisées :
public class CustomerWithConstructorBinding
{
public int Id { get; set; }
public string Name { get; set; }
public CustomerWithConstructorBinding(string name)
{
Name = name;
}
}
Malheureusement, dans certains scénarios, la liaison de constructeur n’est pas une option. Les propriétés de navigation, par exemple, ne peuvent pas être initialisées de cette façon. Dans ces cas, vous pouvez simplement initialiser la propriété sur null
avec l’aide de l’opérateur null-forgiving (mais regardez ci-dessous pour plus d’informations) :
public Product Product { get; set; } = null!;
Propriétés de navigation obligatoires
Les propriétés de navigation obligatoires présentent une difficulté supplémentaire : bien qu’une dépendance existe toujours pour un principal donné, elle peut ou non être chargée par une requête particulière, en fonction des besoins à ce stade du programme (voir les différents modèles de chargement des données). En même temps, il peut être indésirable de rendre ces propriétés nullables, car cela forcerait tout accès à ces derniers à rechercher null
, même lorsque la navigation est connue pour être chargée et ne peut donc pas être null
.
Cela ne pose pas nécessairement de problème ! Tant qu’une dépendance requise est correctement chargée (par exemple, via Include
), l’accès à sa propriété de navigation est garanti pour toujours retourner non-null. En revanche, l’application peut choisir de vérifier si la relation est chargée ou non en vérifiant si la navigation est null
. Dans ce cas, il est raisonnable de rendre la navigation nullable. Cela signifie que les navigations obligatoires du dépendant vers le principal :
- doivent être non-nullables s’il est considéré comme une erreur de programmeur d’accéder à une navigation lorsqu’elle n’est pas chargée ;
- doivent être nullables s’il est acceptable pour le code d’application de vérifier la navigation afin de déterminer si la relation est chargée ou non.
Si vous souhaitez une approche plus stricte, vous pouvez avoir une propriété non-nullable avec un champ de stockage nullable :
private Address? _shippingAddress;
public Address ShippingAddress
{
set => _shippingAddress = value;
get => _shippingAddress
?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
}
Tant que la navigation est correctement chargée, la dépendance est accessible via la propriété. Si, toutefois, la propriété est accessible sans d’abord charger correctement l’entité associée, une InvalidOperationException
est levée, car le contrat d’API a été utilisé de manière incorrecte.
Remarque
Les navigations de collection, qui contiennent des références à plusieurs entités associées, doivent toujours être non-nullables. Une collection vide signifie qu’aucune entité associée n’existe, mais la liste elle-même ne doit jamais être null
.
DbContext et DbSet
Avec EF, il est courant d’avoir des propriétés DbSet non initialisées sur les types de contexte :
public class MyContext : DbContext
{
public DbSet<Customer> Customers { get; set;}
}
Bien que cela provoque généralement un avertissement du compilateur, EF Core 7.0 et versions ultérieures supprime cet avertissement, car EF initialise automatiquement ces propriétés via la réflexion.
Sur l’ancienne version d’EF Core, vous pouvez contourner ce problème comme suit :
public class MyContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
}
Une autre stratégie consiste à utiliser des propriétés automatiques non-nullables et à les initialiser sur null
, en utilisant l’opérateur null-forgiving (!) pour faire taire l’avertissement du compilateur. Le constructeur de base DbContext garantit que toutes les propriétés DbSet seront initialisées et que la valeur Null ne sera jamais observée sur ces propriétés.
Navigation et inclusion de relations nullables
Lorsque vous travaillez sur des relations facultatives, il est possible de rencontrer des avertissements du compilateur où une exception de référence null
réelle serait impossible. Lors de la traduction et de l’exécution de vos requêtes LINQ, EF Core garantit que si une entité associée facultative n’existe pas, toute navigation vers celle-ci est simplement ignorée, au lieu de générer une levée. Toutefois, le compilateur ignore cette garantie EF Core et génère des avertissements comme si la requête LINQ était exécutée en mémoire, avec LINQ to Objects. Par conséquent, il est nécessaire d’utiliser l’opérateur null-forgiving (!) pour informer le compilateur qu’une valeur null
réelle n’est pas possible :
var order = context.Orders
.Where(o => o.OptionalInfo!.SomeProperty == "foo")
.ToList();
Un problème similaire se produit lors de l’inclusion de plusieurs niveaux de relations entre les navigations facultatives :
var order = context.Orders
.Include(o => o.OptionalInfo!)
.ThenInclude(op => op.ExtraAdditionalInfo)
.Single();
Si vous faites cela fréquemment et que les types d’entité en question sont principalement (ou exclusivement) utilisés dans les requêtes EF Core, envisagez de rendre les propriétés de navigation non-nullables et de les configurer comme facultatives via l’API Fluent ou les annotations de données. Cela supprime tous les avertissements du compilateur tout en conservant la relation facultative. Toutefois, si vos entités sont parcourues en dehors d’EF Core, vous pouvez observer des valeurs null
bien que les propriétés soient annotées comme non-nullables.
Limitations dans les versions antérieures
Avant EF Core 6.0, les limitations suivantes s’appliquaient :
- La surface de l’API publique n’était pas annotée pour l’acceptation de la valeur Null (l’API publique était « sans valeur Null »), ce qui la rend parfois difficile à utiliser lorsque la fonctionnalité NRT est activée. Cela inclut notamment les opérateurs LINQ asynchrones exposés par EF Core, tels que FirstOrDefaultAsync. L’API publique est entièrement annotée pour accepter la valeur Null à partir d’EF Core 6.0.
- L’ingénierie à rebours n’a pas pris en charge les types références nullables C# 8 (NRTs) : EF Core a toujours généré du code C# qui supposait que la fonctionnalité était désactivée. Par exemple, les colonnes de texte nullables ont été générées automatiquement en tant que propriété de type
string
, passtring?
, avec l’API Fluent ou les annotations de données utilisées pour définir si une propriété est requise ou non. Si vous utilisez une version antérieure d’EF Core, vous pouvez toujours modifier le code généré automatiquement et remplacer ces éléments par des annotations de nullabilité C#.