Exercice : appliquer des stratégies de sécurité des valeurs Null

Effectué

Dans l’unité précédente, vous avez appris à exprimer l’intention de possibilité de valeur Null dans le code. Dans cette leçon, vous allez appliquer ce que vous avez appris à un projet C# existant.

Notes

Ce module utilise l’interface CLI .NET et Visual Studio Code pour le développement local. À l’issue de ce module, vous pourrez appliquer les concepts avec Visual Studio (Windows) ou Visual Studio pour Mac (macOS), ou poursuivre le développement avec Visual Studio Code (Windows, Linux et macOS).

Ce module utilise le SDK .NET 6.0. Assurez-vous que vous avez installé .NET 6.0 en exécutant la commande suivante dans votre terminal préféré :

dotnet --list-sdks

Une sortie similaire à ce qui suit s’affiche :

3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]

Vérifiez que la liste comporte une version commençant par 6. S’il n’y en a pas ou que la commande est introuvable, installez la dernière version du kit SDK .NET 6.0.

Récupérer et examiner l’exemple de code

  1. Dans un terminal de commande, clonez l’exemple de dépôt GitHub et basculez vers le répertoire cloné.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Ouvrez le répertoire du projet dans Visual Studio Code.

    code .
    
  3. Exécutez le projet d’exemple à l’aide de la commande dotnet run.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    Cela entraînera la levée d’une exception NullReferenceException.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
       at Program.<Main>$(String[] args) in .\src\ContosoPizza.Service\Program.cs:line 13
    

    Le rapport des appels de procédure indique que l’exception s’est produite au niveau de la ligne 13 dans .\src\ContosoPizza.Service\Program.cs. Dans la ligne 13, la méthode Add est appelée sur la propriété pizza.Cheeses. Étant donné que pizza.Cheeses est null, une exception NullReferenceException est levée.

    using ContosoPizza.Models;
    
    // Create a pizza
    Pizza pizza = new("Meat Lover's Special")
    {
        Size = PizzaSize.Medium,
        Crust = PizzaCrust.DeepDish,
        Sauce = PizzaSauce.Marinara,
        Price = 17.99m,
    };
    
    // Add cheeses
    pizza.Cheeses.Add(PizzaCheese.Mozzarella);
    pizza.Cheeses.Add(PizzaCheese.Parmesan);
    
    // Add toppings
    pizza.Toppings.Add(PizzaTopping.Sausage);
    pizza.Toppings.Add(PizzaTopping.Pepperoni);
    pizza.Toppings.Add(PizzaTopping.Bacon);
    pizza.Toppings.Add(PizzaTopping.Ham);
    pizza.Toppings.Add(PizzaTopping.Meatballs);
    
    Console.WriteLine(pizza);
    
    /*
        Expected output:
    
        The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
        It's covered with a blend of mozzarella and parmesan cheese.
        It's layered with sausage, pepperoni, bacon, ham and meatballs.
        This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
    */
    

Activer un contexte pouvant accepter la valeur Null

Vous allez maintenant activer un contexte pouvant accepter la valeur Null et examiner son effet sur la compilation.

  1. Dans src/ContosoPizza. service/ContosoPizza.Service.csproj, ajoutez la ligne en surbrillance et enregistrez vos modifications :

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" />
      </ItemGroup>
    
    </Project>
    

    La modification précédente active le contexte pouvant accepter la valeur Null pour l’ensemble du projet ContosoPizza.Service.

  2. Dans src/ContosoPizza.Models/ContosoPizza.Models.csproj, ajoutez la ligne en surbrillance et enregistrez vos modifications :

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
    </Project>
    

    La modification précédente active le contexte pouvant accepter la valeur Null pour l’ensemble du projet ContosoPizza.Models.

  3. Compiler l’exemple de solution à l’aide de la commande dotnet build.

    dotnet build
    

    La compilation réussit avec deux avertissements.

    dotnet build
    Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      Restored .\src\ContosoPizza.Service\ContosoPizza.Service.csproj (in 477 ms).
      Restored .\src\ContosoPizza.Models\ContosoPizza.Models.csproj (in 475 ms).
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
      ContosoPizza.Models -> .\src\ContosoPizza.Models\bin\Debug\net6.0\ContosoPizza.Models.dll
      ContosoPizza.Service -> .\src\ContosoPizza.Service\bin\Debug\net6.0\ContosoPizza.Service.dll
    
    Build succeeded.
    
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
        2 Warning(s)
        0 Error(s)
    
    Time Elapsed 00:00:07.48
    
  4. Compilez l’exemple de solution à l’aide de la commande une nouvelle fois à l’aide de la commande dotnet build.

    dotnet build
    

    Cette fois-ci, la compilation réussit sans erreurs ni avertissements. La compilation précédente s’était terminée avec succès, avec des avertissements. Étant donné que la source n’a pas changé, le processus de compilation ne réexécute pas le compilateur. Étant donné que la compilation n’exécute pas le compilateur, les avertissements ont disparu.

    Conseil

    Vous pouvez forcer une recompilation de tous les assemblys d’un projet à l’aide de la commande dotnet clean avant d’exécuter dotnet build.

  5. Dans les fichiers .csproj, ajoutez les lignes en surbrillance et enregistrez vos modifications.

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" />
      </ItemGroup>
    
    </Project>
    
    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
    </Project>
    

    Les modifications précédentes indiquent au compilateur de faire échouer la compilation chaque fois qu’un avertissement est rencontré.

    Conseil

    L’utilisation de <TreatWarningsAsErrors> est facultative. Toutefois, nous vous la recommandons, car cela vous permet de passer en revue tous les avertissements.

  6. Compiler l’exemple de solution à l’aide de la commande dotnet build.

    dotnet build
    

    La compilation échoue avec 2 erreurs.

    dotnet build
    Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      All projects are up-to-date for restore.
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    
    Build FAILED.
    
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
        0 Warning(s)
        2 Error(s)
    
    Time Elapsed 00:00:02.95
    

    Lors du traitement des avertissements en tant qu’erreurs, l’application n’est plus compilée. Cela est en fait souhaité dans cette situation, car le nombre d’erreurs est faible et nous les traiterons rapidement. Les deux erreurs (CS8618) vous permettent de savoir qu’il existe des propriétés déclarées non-nullables qui n’ont pas encore été initialisées.

Corriger les erreurs

Il existe beaucoup de méthodes pour résoudre les avertissements/erreurs liés à la possibilité de valeur Null. Voici quelques exemples :

  • Exiger une collection non-nullable de fromages et de garnitures comme paramètres du constructeur
  • Intercepter la propriété get/set et ajoutez une vérification null
  • Exprimer l’intention de possibilité de valeur Null pour les propriétés
  • Initialiser la collection avec une valeur inlined (vide) par défaut à l’aide d’initialiseurs de propriété
  • Attribuer à la propriété une valeur (vide) par défaut dans le constructeur
  1. Pour corriger l’erreur sur la propriété Pizza.Cheeses, modifiez la définition de la propriété sur Pizza.cs pour ajouter une vérification null. Une pizza n’est pas vraiment une pizza sans fromage.

    namespace ContosoPizza.Models;
    
    public sealed record class Pizza([Required] string Name)
    {
        private ICollection<PizzaCheese>? _cheeses;
    
        public int Id { get; set; }
    
        [Range(0, 9999.99)]
        public decimal Price { get; set; }
    
        public PizzaSize Size { get; set; }
        public PizzaCrust Crust { get; set; }
        public PizzaSauce Sauce { get; set; }
    
        public ICollection<PizzaCheese> Cheeses
        {
            get => (_cheeses ??= new List<PizzaCheese>());
            set => _cheeses = value ?? throw new ArgumentNullException(nameof(value));
        }
    
        public ICollection<PizzaTopping>? Toppings { get; set; }
    
        public override string ToString() => this.ToDescriptiveString();
    }
    

    Dans le code précédent :

    • Un nouveau champ de stockage a été ajouté pour faciliter l’interception des accesseurs de propriété get et set nommés _cheeses. Il est déclaré comme pouvant accepter la valeur Null (?) et reste non initialisé.
    • L’accesseur get est mappé à une expression qui utilise l’opérateur de coalescence nulle (??). Cette expression retourne le champ _cheeses, en supposant qu’il ne s’agit pas de null. S’il s’agit de null, il affecte _cheeses à new List<PizzaCheese>() avant de retourner _cheeses.
    • L’accesseur set est également mappé à une expression et utilise l’opérateur de coalescence nulle. Lorsqu’un consommateur attribue une valeur null, l’exception ArgumentNullException est levée.
  2. Dans la mesure où toutes les pizzas n’ont pas de garniture personnalisable, null peut constituer une valeur valide pour la propriété Pizza.Toppings. Dans ce cas, il est logique que la valeur puisse accepter la valeur Null.

    1. Modifiez la définition de propriété dans Pizza.cs pour autoriser Toppings à avoir une valeur Null.

      namespace ContosoPizza.Models;
      
      public sealed record class Pizza([Required] string Name)
      {
          private ICollection<PizzaCheese>? _cheeses;
      
          public int Id { get; set; }
      
          [Range(0, 9999.99)]
          public decimal Price { get; set; }
      
          public PizzaSize Size { get; set; }
          public PizzaCrust Crust { get; set; }
          public PizzaSauce Sauce { get; set; }
      
          public ICollection<PizzaCheese> Cheeses
          {
              get => (_cheeses ??= new List<PizzaCheese>());
              set => _cheeses = value ?? throw new ArgumentNullException(nameof(value));
          }
      
          public ICollection<PizzaTopping>? Toppings { get; set; }
      
          public override string ToString() => this.ToDescriptiveString();
      }
      

      La propriété Toppings est désormais exprimée comme acceptant la valeur Null.

    2. Ajoutez la ligne en surbrillance à ContosoPizza.Service\Program.cs :

      using ContosoPizza.Models;
      
      // Create a pizza
      Pizza pizza = new("Meat Lover's Special")
      {
          Size = PizzaSize.Medium,
          Crust = PizzaCrust.DeepDish,
          Sauce = PizzaSauce.Marinara,
          Price = 17.99m,
      };
      
      // Add cheeses
      pizza.Cheeses.Add(PizzaCheese.Mozzarella);
      pizza.Cheeses.Add(PizzaCheese.Parmesan);
      
      // Add toppings
      pizza.Toppings ??= new List<PizzaTopping>();
      pizza.Toppings.Add(PizzaTopping.Sausage);
      pizza.Toppings.Add(PizzaTopping.Pepperoni);
      pizza.Toppings.Add(PizzaTopping.Bacon);
      pizza.Toppings.Add(PizzaTopping.Ham);
      pizza.Toppings.Add(PizzaTopping.Meatballs);
      
      Console.WriteLine(pizza);
      
      /*
          Expected output:
      
          The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
          It's covered with a blend of mozzarella and parmesan cheese.
          It's layered with sausage, pepperoni, bacon, ham and meatballs.
          This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
      */
      

    Dans le code précédent, l’opérateur de coalescence nulle a été utilisé pour attribuer Toppings à new List<PizzaTopping>(); s’il s’agit de null.

Exécuter la solution finale

  1. Enregistrez vos modifications, puis compilez la solution.

    dotnet build
    

    La compilation se termine sans erreurs ni avertissements.

  2. Exécutez l’application.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    L’application s’exécute jusqu’à la fin (sans erreur) et affiche la sortie suivante :

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
    It's covered with a blend of mozzarella and parmesan cheese.
    It's layered with sausage, pepperoni, bacon, ham and meatballs.
    This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
    

Résumé

Dans cette leçon, vous avez utilisé un contexte pouvant accepter la valeur Null pour identifier et empêcher les occurrences potentielles de NullReferenceException dans votre code.