Utilisation des types référence nullables
C# 8 a introduit une nouvelle fonctionnalité appelée types de référence nullables (NRT), permettant d’annoter les types de référence, indiquant s’il est valide pour eux de contenir ou non null. Si vous débutez avec 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, sauf si vous y avez explicitement opté.
Cette page présente la prise en charge d’EF Core pour les types référence 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érence nullables est la page Propriétés obligatoires et facultatives . Il est recommandé de commencer par lire cette page.
Notes
Soyez prudent lorsque vous activez des types référence nullables sur un projet existant : les propriétés de type référence qui étaient précédemment configurées comme facultatives seront désormais configurées en fonction des besoins, sauf si elles sont explicitement annotées pour être nullables. 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 la possibilité de valeur Null de la colonne de base de données.
Propriétés non nullables et initialisation
Lorsque les types référence nullables sont activés, le compilateur C# émet des avertissements pour toute propriété non nullable non initialisée, car celles-ci contiennent null. Par conséquent, la méthode courante d’écriture des types d’entités suivante 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 une version ultérieure, les membres requis fournissent la solution parfaite à ce problème :
public required string Name { get; set; }
Le compilateur garantit désormais 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é n’est pas nullable, toutes les instances chargées par EF contiennent toujours un nom non null.
Si vous utilisez une version antérieure de C#, la liaison de constructeur est une technique alternative 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 ce cas, vous pouvez simplement initialiser la propriété sur null à l’aide de l’opérateur null-forgiving (mais voir ci-dessous pour plus d’informations) :
public Product Product { get; set; } = null!;
Propriétés de navigation requises
Les propriétés de navigation requises présentent une difficulté supplémentaire : bien qu’il existe toujours un dépendant pour un principal donné, il peut ou non être chargé 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). Dans le même temps, il n’est pas souhaitable de rendre ces propriétés nullables, car cela obligerait tous les accès à celles-ci à vérifier la valeur Null, même si elles sont requises.
Ce n’est pas nécessairement un problème ! Tant qu’un dépendant requis est correctement chargé (par exemple, via Include
), l’accès à sa propriété de navigation est garanti pour toujours retourner une valeur non null. En revanche, l’accès à une propriété de navigation sans charger d’abord le dépendant est une erreur du programmeur ; en tant que tel, il peut être acceptable que la propriété de navigation retourne null et quand le code (buggy) lève un NullReferenceException
. Après tout, le code utilise EF de manière incorrecte.
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, le dépendant est accessible via la propriété . Toutefois, si la propriété est accessible sans avoir au préalable chargé correctement l’entité associée, une InvalidOperationException
est levée, car le contrat d’API a été utilisé de manière incorrecte.
Notes
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’il n’existe aucune entité associée, mais que la liste elle-même ne doit jamais être null.
DbContext et DbSet
La pratique courante d’avoir des propriétés DbSet non initialisées sur les types de contexte est également problématique, car le compilateur émet désormais des avertissements pour ceux-ci. Ce problème peut être résolu comme suit :
public class NullableReferenceTypesContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFNullableReferenceTypes;Trusted_Connection=True");
}
Une autre stratégie consiste à utiliser des propriétés automatiques non nullables, mais à 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 celles-ci.
Navigation et inclusion de relations nullables
Lorsque vous traitez des relations facultatives, il est possible de rencontrer des avertissements du compilateur lorsqu’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 aucune entité associée facultative n’existe, toute navigation vers celle-ci sera simplement ignorée, au lieu de lever. Toutefois, le compilateur ne connaît pas 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 :
Console.WriteLine(order.OptionalInfo!.ExtraAdditionalInfo!.SomeExtraAdditionalInfo);
Un problème similaire se produit lors de l’inclusion de plusieurs niveaux de relations dans les navigations facultatives :
var order = context.Orders
.Include(o => o.OptionalInfo!)
.ThenInclude(op => op.ExtraAdditionalInfo)
.Single();
Si vous le faites beaucoup et que les types d’entités 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 facultatifs via l’API Fluent ou les annotations de données. Cela supprimera tous les avertissements du compilateur tout en conservant la relation facultative ; Toutefois, si vos entités sont traversées 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’appliquait :
- La surface de l’API publique n’a pas été annotée pour la possibilité de null (l’API publique était « null-oblivious »), ce qui rend parfois difficile l’utilisation 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 la possibilité de null à partir d’EF Core 6.0.
- L’ingénierie inverse ne prenait pas en charge les types de référence nullables (NRT) C# 8 : EF Core générait toujours 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#.