Ejercicio: Aplicación de estrategias de seguridad de valores NULL

Completado

En la unidad anterior, ha aprendido a expresar la intención de nulabilidad en el código. En esta unidad, aplicará lo que ha aprendido a un proyecto de C# existente.

Nota:

En este módulo se usan la CLI (interfaz de la línea de comandos) de .NET y Visual Studio Code para el desarrollo local. Cuando complete este módulo, podrá aplicar los conceptos mediante Visual Studio (Windows), Visual Studio para Mac (macOS), o seguir con el desarrollo mediante Visual Studio Code (Windows, Linux y macOS).

En este módulo se usa el SDK de .NET 6.0. Asegúrese de que tiene instalado .NET 6.0 mediante la ejecución del siguiente comando en la terminal que prefiera:

dotnet --list-sdks

Verá un resultado similar al siguiente:

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

Asegúrese de que aparezca una versión que comience en 6. Si no aparece ninguna o no se encuentra el comando, instale el SDK más reciente de .NET 6.0.

Recuperación y examen del código de ejemplo

  1. En un terminal de comandos, clone el repositorio de GitHub ejemplo y cambie al directorio clonado.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Abra el directorio del proyecto en Visual Studio Code.

    code .
    
  3. Ejecute el proyecto de ejemplo mediante el comando dotnet run.

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

    Como resultado, se produce 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
    

    El seguimiento de la pila indica que la excepción se produjo en la línea 13 de .\src\ContosoPizza.Service\Program.cs. En la línea 13, se llama al método Add en la propiedad pizza.Cheeses. Puesto que pizza.Cheeses es null, se produce NullReferenceException.

    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!
    */
    

Habilitación de un contexto que acepta valores NULL

Ahora habilitará un contexto que acepta valores NULL y examinará su efecto en la compilación.

  1. En src/ContosoPizza.Service/ContosoPizza.Service.csproj, agregue la línea resaltada y guarde los cambios:

    <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>
    

    El cambio anterior habilita el contexto que acepta valores NULL para todo el proyecto ContosoPizza.Service.

  2. En src/ContosoPizza.Models/ContosoPizza.Models.csproj, agregue la línea resaltada y guarde los cambios:

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

    El cambio anterior habilita el contexto que acepta valores NULL para todo el proyecto ContosoPizza.Models.

  3. Compile la solución de ejemplo mediante el comando dotnet build.

    dotnet build
    

    La compilación se realiza correctamente con dos advertencias.

    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. Compile la solución de ejemplo usando de nuevo el comando dotnet build.

    dotnet build
    

    Esta vez, la compilación se realiza correctamente sin errores ni advertencias. La compilación anterior se completó correctamente, con advertencias. Puesto que el origen no cambió, el proceso de compilación no vuelve a ejecutar el compilador. Puesto que la compilación no ejecuta el compilador, no hay advertencias.

    Sugerencia

    Puede forzar una recompilación de todos los ensamblados de un proyecto mediante el comando dotnet clean anterior a dotnet build.

  5. En los archivos .csproj, agregue las líneas resaltadas y guarde los cambios.

    <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>
    

    Los cambios anteriores indicarán al compilador que genere un error en la compilación cada vez que se encuentre una advertencia.

    Sugerencia

    El uso de <TreatWarningsAsErrors> es opcional. Sin embargo, es recomendable, ya que garantiza que no pase por alto las advertencias.

  6. Compile la solución de ejemplo mediante el comando dotnet build.

    dotnet build
    

    Se produce un error en la compilación con dos errores.

    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
    

    Al tratar las advertencias como errores, la aplicación ya no se compila. De hecho, esto es lo que se desea en esta situación, ya que el número de errores es pequeño y los abordaremos rápidamente. Los dos errores (CS8618) le permiten saber que hay propiedades declaradas como que no aceptan valores NULL que aún no se han inicializado.

Corrección de errores

Hay una gran cantidad de tácticas para resolver las advertencias o los errores relacionados con la nulabilidad. Estos son algunos ejemplos:

  • Requerir una colección que no acepta valores NULL de queso e ingredientes como parámetros de constructor
  • Interceptar la propiedad get/set y agregue una comprobación de null
  • Expresar la intención de que las propiedades acepten valores NULL
  • Inicializar la colección con un valor predeterminado (vacío) insertado mediante inicializadores de propiedad
  • Asignar a la propiedad un valor predeterminado (vacío) en el constructor
  1. Para corregir el error en la propiedad Pizza.Cheeses, modifique la definición de propiedad en Pizza.cs para agregar una comprobación de null. No es realmente una pizza sin queso, ¿verdad?

    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();
    }
    

    En el código anterior:

    • Se agrega un nuevo campo de respaldo para ayudar a interceptar los descriptores de acceso de propiedad get y set denominados _cheeses. Se declara como que acepta valores NULL (?) y se deja sin inicializar.
    • El descriptor de acceso get se asigna a una expresión que usa el operador de fusión de NULL (??). Esta expresión devuelve el campo _cheeses, suponiendo que no sea null. Si es null, se asigna _cheeses a new List<PizzaCheese>() antes de devolver _cheeses.
    • El descriptor de acceso set también se asigna a una expresión y usa el operador de fusión de NULL. Cuando un consumidor asigna un valor null, se produce ArgumentNullException.
  2. Puesto que no todas las pizzas tienen ingredientes, null puede ser un valor válido para la propiedad Pizza.Toppings. En este caso, tiene sentido expresarlo como que acepta valores NULL.

    1. Modifique la definición de propiedad en Pizza.cs para permitir que Toppings acepte valores 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 propiedad Toppings ahora se expresa como que acepta valores NULL.

    2. Agregue la línea resaltada a 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!
      */
      

    En el código anterior, el operador de fusión de NULL se usa para asignar Toppings a new List<PizzaTopping>(); si es null.

Ejecución de la solución completada

  1. Guarde todos los cambios y, a continuación, compile la solución.

    dotnet build
    

    La compilación se completa sin errores ni advertencias.

  2. Ejecutar la aplicación.

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

    La aplicación se ejecuta hasta su finalización (sin errores) y muestra la siguiente salida:

    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!
    

Resumen

En esta unidad, ha usado un contexto que acepta valores NULL para identificar y evitar posibles ocurrencias de NullReferenceException en el código.