Exercício – aplicar estratégias de segurança nula

Concluído

Na unidade anterior, você aprendeu a expressar a intenção de nulidade no código. Nesta unidade, você aplicará o que aprendeu a um projeto C# existente.

Observação

Este módulo usa a CLI (interface de linha de comando) do .NET e o Visual Studio Code para desenvolvimento local. Depois de concluir este módulo, você pode aplicar os conceitos usando o Visual Studio (Windows), o Visual Studio para Mac (macOS) ou o desenvolvimento contínuo usando o Visual Studio Code (Windows, Linux, & macOS).

Este módulo usa o SDK do .NET 6.0. Verifique se tem o .NET 6.0 instalado, executando o seguinte comando em seu terminal preferido:

dotnet --list-sdks

Saída semelhante à seguinte exibida:

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

Verifique se uma versão que começa com 6 está listada. Se nenhum estiver listado ou o comando não for encontrado, instale o SDK do .NET 6.0 mais recente.

Recuperar e examinar o código de exemplo

  1. Em um terminal de comando, clone o exemplo de repositório do GitHub e alterne para o diretório clonado.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Abra o diretório do projeto no Visual Studio Code.

    code .
    
  3. Execute o projeto de exemplo usando o comando dotnet run.

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

    Isso resultará na geração de 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
    

    O rastreamento de pilha indica que a exceção ocorreu na linha 13 em .\src\ContosoPizza.Service\Program.cs. Na linha 13, o método Add é chamado na propriedade pizza.Cheeses. Como pizza.Cheeses é null, é gerado 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!
    */
    

Habilitar contexto anulável

Agora você habilitará um contexto anulável e examinará seu efeito na compilação.

  1. Em src/ContosoPizza.Service/ContosoPizza.Service.csproj, adicione a linha realçada e salve as alterações:

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

    A alteração anterior habilita o contexto anulável para todo o projeto do ContosoPizza.Service.

  2. Em src/ContosoPizza.Models/ContosoPizza.Models.csproj, adicione a linha realçada e salve as alterações:

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

    A alteração anterior habilita o contexto anulável para todo o projeto do ContosoPizza.Models.

  3. Crie a solução de exemplo usando o comando dotnet build.

    dotnet build
    

    A compilação tem êxito com dois avisos.

    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. Crie a solução de exemplo novamente usando o comando dotnet build.

    dotnet build
    

    Desta vez, a compilação tem êxito sem erros ou avisos. A compilação anterior foi concluída com êxito, com avisos. Como a origem não foi alterada, o processo de compilação não executa o compilador novamente. Como a compilação não executa o compilador, não há nenhum aviso.

    Dica

    Você pode forçar uma recompilação de todos os assemblies em um projeto usando o comando dotnet clean anterior ao dotnet build.

  5. Nos arquivos .csproj, adicione as linhas realçadas e salve as alterações.

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

    As alterações anteriores instruem o compilador a falhar na compilação sempre que um aviso é encontrado.

    Dica

    O uso de <TreatWarningsAsErrors> é opcional. No entanto, recomendamos isso, pois garante que você não ignore nenhum aviso.

  6. Crie a solução de exemplo usando o comando dotnet build.

    dotnet build
    

    A compilação falha com 2 erros.

    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
    

    Ao tratar avisos como erros, o aplicativo não é mais compilado. Isso, na verdade, é desejado nessa situação, pois o número de erros é pequeno e nós os resolveremos rapidamente. Os dois erros (CS8618) permitem que você saiba que há propriedades declaradas como não anuláveis que ainda não foram inicializadas.

Corrigir os erros

Há várias táticas para resolver os avisos/os erros relacionados à nulidade. Alguns exemplos incluem:

  • Exigir uma coleção não anulável de queijos e coberturas como parâmetros do construtor
  • Interceptar a propriedade get/set e adicionar uma verificação de null
  • Expressar a intenção de que as propriedades sejam anuláveis
  • Inicializar a coleção com um valor padrão (vazio) embutido usando inicializadores de propriedade
  • Atribuir um valor padrão (vazio) à propriedade no construtor
  1. Para corrigir o erro na propriedade Pizza.Cheeses, modifique a definição de propriedade em Pizza.cs para adicionar uma verificação de null. Uma pizza não é realmente uma pizza sem queijo, não é?

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

    No código anterior:

    • Um novo campo de apoio é adicionado para ajudar a interceptar os acessadores de propriedade get e set chamados de _cheeses. Ele é declarado como anulável (?) e deixado sem inicializar.
    • O acessador get é mapeado para uma expressão que usa o operador de avaliação de nulo (??). Essa expressão retorna o campo _cheeses, supondo que não seja null. Se for null, ele atribuirá _cheeses como new List<PizzaCheese>() antes de retornar _cheeses.
    • O acessador set também é mapeado para uma expressão e faz uso do operador de avaliação de nulo. Quando um consumidor atribui um valor null, o ArgumentNullException é gerado.
  2. Como nem todas as pizzas têm cobertura, null pode ser um valor válido para a propriedade Pizza.Toppings. Nesse caso, faz sentido expressá-la como anulável.

    1. Modifique a definição de propriedade em Pizza.cs para permitir que Toppings seja anulável.

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

      A propriedade Toppings agora é expressa como anulável.

    2. Adicione a linha realçada 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!
      */
      

    No código anterior, o operador de avaliação de nulo é usado para atribuir Toppings a new List<PizzaTopping>(); se for null.

Executar a solução concluída

  1. Salve todas as suas alterações e, em seguida, compile a solução.

    dotnet build
    

    A compilação é concluída sem erros nem avisos.

  2. Execute o aplicativo.

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

    O aplicativo é executado até a conclusão (sem erro) e exibe a seguinte saída:

    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!
    

Resumo

Nesta unidade, você usou um contexto anulável para identificar e evitar possíveis ocorrências de NullReferenceException no código.