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 débutez avec cette fonctionnalité, il est recommandé de vous familiariser avec elle en lisant les documents C#. Les types références 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 elle est explicitement choisie.

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.

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, pas string?, 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#.