Novidades do .NET 5

André Bessa
André Bessa

Compartilhe

Três pessoas reunidas em frente de dois computadores programando.

Em 2019 a Microsoft anunciava que após o lançamento do .NET 3.0, sua próxima evolução seria o .NET 5, uma versão que unifica o .NET Framework com o .NET Core em uma única plataforma.

“.NET 5”.

Agora com apenas um ecossistema podemos direcionar o desenvolvimento de aplicações para Windows, MacOS, Linux, OS, Android, tvOS, watchOS e outras plataformas de uma forma mais transparente.

O.NET 5 não substitui o .NET Framework 4.x, que ainda terá suporte pela Microsoft por algum tempo, mas de agora em diante o .NET 5 será a principal implementação do .NET, pelo menos até a chegada do .NET 6 😎

Mas e você que já usava outra versão do .NET e acabou de instalar o .NET 5, está sentindo falta de algum recurso da plataforma? Ou quer saber de alguma novidade para deixar o desenvolvimento com C# mais dinâmico? Ou ainda, você começou a utilizar o .NET 5 e simplesmente quer saber como deixar seu projeto atualizado com as últimas novidades dele? Então, vamos ao artigo.

C# versão 9

Junto com a plataforma .NET 5 a Microsoft lança o C# 9, hoje a principal linguagem do .NET, que adiciona e aprimora a linguagem com recursos interessantes:

  • Um novo tipo: Nessa nova versão da linguagem foi introduzido o tipo record, uma estrutura, que permite definir um tipo de referência para encapsular dados e dar suporte a criação de tipos com propriedades imutáveis, além de compará-lo como se fosse um tipo de valor.

No exemplo abaixo, tipamos o objeto Pessoa como record...

public record Pessoa{
   public string Nome { get; set; }
   public string Identificador { get; set; }
   public string Email { get; set; }
}

Foram definidos dois objetos Pessoa que são do tipo record. Em uma comparação, observamos seus valores, como no código abaixo, em que verificamos a igualdade com o método Equals():

Pessoa pessoa1 = new(){
               Nome="André Silva",
               Identificador="12345678",
               Email="[email protected]"
           };

Pessoa pessoa2 = new(){
               Nome="André Silva",
               Identificador="12345678",
               Email="[email protected]"
           };

Console.WriteLine(Equals(pessoa1,pessoa2)); //true

Para o mesmo exemplo, se alteramos de public record Pessoa para public class Pessoa, por ser a classe um tipo de referência, o resultado Console.WriteLine(Equals(pessoa1,pessoa2)) será false.

Outro recurso interessante na utilização de record é a expressão with, muito útil, pois com ela conseguimos criar um objeto com base em outro que já existe. Veja no exemplo abaixo:

Pessoa pessoa1 = new(){
               Nome="André Silva",
               Identificador="12345678",
               Email="[email protected]"
           };

Pessoa pessoa2 = pessoa1 with {Nome="Pedro Malazartes"};

Console.WriteLine(pessoa2); 

//Pessoa { Nome = Pedro Malazartes, Identificador = 12345678, Email = [email protected] }

Com a expressão with foi possível criar o objeto pessoa2 alterando somente o valor da propriedade nome e aproveitar os valores das outras propriedades do objeto pessoa1.

  • Setter somente inicialização: Com o uso da palavra reservada init definimos que apenas quando fizermos a inicialização do objeto poderemos atribuir valores às propriedades que servem somente para leitura. No trecho de código seguinte, só poderemos atribuir valores para Nome, Identificador e Email quando inicializarmos Pessoa.
public record Pessoa{
   public string Nome { get; init; }
   public string Identificador { get; init; }
   public string Email { get; init; }
}

Agora inicializamos as propriedades durante a criação do objeto.

Pessoa p = new Pessoa{
   Name="André Silva",
   Identificador="12345678",
   Email="[email protected]"
};

Se depois da inicialização alterarmos alguma informação recebida pelo objeto, ocorrerá um erro de compilação.

Na imagem a propriedade Name `p.Name` aparece sublinhada, como o editor de texto pode mostrá-la quando acontece um erro de compilação na atribuição de um novo valor para ela.

  • Código mais enxuto com Instruções de Nível Superior : O C# 9 trouxe como recurso não precisarmos mais escrever explicitamente o método Main() em projetos do tipo Console.. Veja o código abaixo:
using System;

namespace Novidades
{
   class Program
   {
       static void Main(string[] args)
       {
           Console.WriteLine("Novidades do .NET 5");
       }
   }
}

Todo esse trecho tem o mesmo resultado da simplificação que apresentamos em:

using System;

Console.WriteLine("Novidades do .NET 5");

ou

System.Console.WriteLine("Novidades do .NET 5");

Mas por que este recurso é interessante? Ele permite a escrita de códigos menores e mais simples, o que pode facilitar a aprendizagem da linguagem por iniciantes. É útil também para criação de pequenos utilitários como Azure Functions e GitHub Actions.

Devemos nos atentar, pois para utilizar a instrução de nível superior é importante que a aplicação tenha somente um ponto de entrada. A adição de mais de uma instrução de nível superior no projeto acarreta um erro de compilação CS8802- Somente uma unidade de compilação pode ter instruções de nível superior.

  • Simplificando a utilização do new : Com objetivo de simplificar ainda mais a escrita de código C#, podemos omitir o tipo da expressão new quando já conhecemos o tipo do objeto a ser criado,. Vamos ao exemplo, no qual já sabemos que o objeto pessoa1 é do tipo Pessoa e portanto, podemos omitir o tipo de new:
Pessoa pessoa1 = new(){
               Nome="André Silva",
               Identificador="12345678",
               Email="[email protected]"
           };

ou simplesmente:

Pessoa pessoa2 = new();

F# versão 5

O F# é a linguagem funcional da Microsoft e com a chegada do .NET 5.também recebeu uma atualização com a adição do recurso:

  • Interpolação de Strings: Agora o F# também possui esse recurso, semelhante à interpolação de cadeias de caracteres que encontramos no C# e no JavaScript. Com ele podemos colocar valores em determinadas posições de uma string sem precisar fazer algum tipo de concatenação. Veja um exemplo:
let usuario = "andresilva"
let mensagem = $" Seja bem-vindo {usuario}."

Atualizações para o Visual Basic

Para pessoas que já trabalham com VB, o .NET 5 não adicionou nenhum novo recurso à linguagem. Contudo, ele trouxe algumas atualizações para dar suporte estendido na utilização de modelos de projetos via CLI.

DescriçãoParâmetro (dotnet new)
Aplicativo do Consoleconsole
Biblioteca de classesclasslib
Aplicativo WPFwpflib
Biblioteca de controles personalizados wpfwpfcustomcontrollib
Windows Aplicativo Forms (WinForms)winforms
Windows Biblioteca de classes(WinForms)winformslib
Projeto de Teste de Unidademstest
Projeto de Teste NUnit 3nunit
Item de Teste NUnit 3nunit-test
Projeto de teste xUnitxunit

Novos recursos para System.Text.Json

O System.Text.Json é o namespace (um conjunto organizado de classes) que a plataforma disponibiliza para trabalhar com serialização e desserialização de JSON, e dentro das novidades do .net 5 esse namespace recebeu uma série de novas opções.

  • Preservar e manipular referências circulares: Para realizar a manutenção de referências circulares foi definida a propriedade public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get; set; } que deve ser definida como Preserve. Sendo assim:

    • Ao serializar: Para tipos mais complexos, o objeto serializador grava as propriedades $id , $values e $ref (metadados).

    • Ao desserializar: para o processo de desserialização os metadados são utilizados pelo objeto desserializador em uma tentativa de compreender, contudo os metadados esperados não são obrigatórios de serem passados neste processo.

Vamos ver um exemplo de utilização deste recurso:

...
public class Funcionario
   {
       public string Nome { get; set; }
       public Funcionario Gerente { get; set; }
       public List<Funcionario> Relatorio { get; set; }
   }

   public class Program
   {
       public static void Main(string[] args)
       {
           Funcionario silva = new(){Nome = "Andre Silva"};

           Funcionario pedro = new(){Nome = "Pedro Parker"};

           silva.Relatorio = new List<Funcionario> { pedro };
           pedro.Gerente = silva;

           JsonSerializerOptions options = new()
           {
               ReferenceHandler = ReferenceHandler.Preserve,
               WriteIndented = true
           };

           string silvaJson = JsonSerializer.Serialize(silva, options);
           Console.WriteLine($"Silva serializado:\n{silvaJson}");

           Funcionario silvaJsonDesserializado =
               JsonSerializer.Deserialize<Funcionario>(silvaJson, options);          
       }
   }

Como saída teremos o seguinte resultado:

Silva serializado:
{
  "$id": "1",
  "Nome": "Andre Silva",
  "Gerente": null,
  "Relatorio": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Nome": "Pedro Parker",
        "Gerente": {
          "$ref": "1"
        },
        "Relatorio": null
      }
    ]
  }
}

Porém, de acordo com a Microsoft este recurso não se aplica para preservar tipos de valor ou tipos imutáveis (strings). A System.Text.Json. para fazer comparações entre objetos, utiliza-se o ReferenceEqualityComparer.Instance, com o qual a igualdade é entendida por referência, não por valor. Veja um exemplo que dá prosseguimento ao código anterior:

...
Console.WriteLine(silvaJsonDesserializado.Relatorio[0].Gerente==                              
                                                       silvaJsonDesserializado); //true

Para mais detalhes, acesse a documentação oficial.

  • Métodos de extensão HttpClient e HttpContent: Como a serialização de objetos Json é uma atividade comum, o .NET trouxe alguns métodos de extensão para HttpClient e HttpContent que permite realizar operações sobre um Json, com apenas uma linha de código. Veja o exempĺo abaixo usando GetFromJsonAsync e PostAsJsonAsync da classe HttpClientJsonExtensions:
...
public class Usuario
   {
       public int Id { get; set; }
       public string Name { get; set; }
       public string Username { get; set; }
       public string Email { get; set; }
   }

   public class Program
   {
       public static async Task Main()
       {
           using HttpClient httpClient = new()
           {
               BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
           };

       Usuario user = await httpClient.GetFromJsonAsync<Usuario>("users/2");
           Console.WriteLine($"Id: {user.Id}");
           Console.WriteLine($"Nome: {user.Name}");
           Console.WriteLine($"Usuário: {user.Username}");
           Console.WriteLine($"Email: {user.Email}");
...
  • Gravação de números entre aspas: Como novidade a System.Text.Json adicionou a possibilidade de serializar números entre aspas nos Jsons de entradas utilizando a propriedade public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get; set; }. Veja um exemplo:
...
Cliente cliente = new()
           {
              Nome  = "André Silva",
              Idade = 30,
              Cpf   = "111.222.555-33"
           };

           JsonSerializerOptions options = new()
           {
               NumberHandling =
                   JsonNumberHandling.AllowReadingFromString |
                   JsonNumberHandling.WriteAsString,
               WriteIndented = true
           };

           string clienteJson =
               JsonSerializer.Serialize<Cliente>(cliente, options);

           Console.WriteLine($"JSON:\n{clienteJson}");

           Cliente clienteJsonDesserializado =
               JsonSerializer.Deserialize<Cliente>(clienteJson, options);

           Console.WriteLine($"Nome: {clienteJsonDesserializado.Nome}");
           Console.WriteLine($"Idade: {clienteJsonDesserializado.Idade}");
           Console.WriteLine($"CPF: {clienteJsonDesserializado.Cpf}");
...

Serializando o objeto cliente para Json:

JSON:
{
  "Nome": "Andre Silva",
  "Idade": "30",
  "Cpf": "111.222.555-33"
}

Desserializando o objeto de Json para um objeto:

Nome: Andre Silva
Idade: 30
CPF: 111.222.555-33
  • Tipos e registros imutáveis: Através de um método construtor público a System.Text.Json permite a desserialização de classes ou estruturas imutáveis. Mas precisamos lembrar de que em classes com mais de um construtor deve-se usar o atributo [JsonConstructor]. Nas classes com um único construtor, será usado o construtor padrão. Veja o código abaixo:
   public struct Dependente
   {
       public string Nome { get; }
       public int Idade { get; }
       public string Parentesco { get; }
       public DateTime DataNascimento { get; }

       [JsonConstructor]
       public Dependente(DateTime dataNascimento, int idade, string nome, string parentesco) =>
       (DataNascimento, Idade, Nome,Parentesco) = (dataNascimento, idade, nome,parentesco);

   }

   public class Program
   {
       public static void Main()
       {
           var json = @"{""nome"":""Penelope"",
                         ""idade"":36,
                         ""parentesco"":""filha"",
                         ""dataNascimento"":""1985-09-06T11:31:01.923395-07:00""} ";
           Console.WriteLine($"Input JSON: {json}");

           var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

           var dependenteJsonDesserializado = JsonSerializer.Deserialize<Dependente>(json, options);

           Console.WriteLine($"Nome: {dependenteJsonDesserializado.Nome}");
           Console.WriteLine($"Idade: {dependenteJsonDesserializado.Idade}");
           Console.WriteLine($"Parentesco: {dependenteJsonDesserializado.Parentesco}");
           Console.WriteLine($"Data Nasc.:  {dependenteJsonDesserializado.DataNascimento}" );

           var retornandoParaJson = JsonSerializer.Serialize<Dependente>(dependenteJsonDesserializado, options);

           Console.WriteLine($"JSON de Saída: {retornandoParaJson}");

       }
   }

Como saída da execução deste código teremos:

JSON de Entrada: {"nome":"Penelope","idade":36,"parentesco":"filha","dataNascimento":"1985-09-06T11:31:01.923395-07:00"} 
Nome: Penelope
Idade: 36
Parentesco: filha
Data Nasc.:  9/6/1985 3:31:01 PM
JSON de Saída: {"nome":"Penelope","idade":36,"parentesco":"filha","dataNascimento":"1985-09-06T15:31:01.923395-03:00"}
  • Inclusão de campos para serialização: Também temos como recurso poder adicionar campos aos processos de serialização e desserialização, por meio do uso do JsonSerializerOptions.IncludeFields e do atributo [JsonInclude] possibilitando informarmos quais campos queremos serializar. No exemplo a seguir, temos uma forma de utilização:
   public class Pessoa
   {
       public string Nome;
       public string Email;
       public string Cpf;
   }

   public class Pessoa2
   {
       [JsonInclude]
       public string Nome;

       [JsonInclude]
       public string Email;

       [JsonInclude]
       public string Cpf;
   }

   public class Program
   {
       public static void Main()
       {
           var json =
               @"{""Nome"":""Andre Silva"",""Email"":""[email protected]"",""Cpf"":""123456-99""} ";
           Console.WriteLine($"JSON de entrada: {json}");

           var options = new JsonSerializerOptions
           {
               IncludeFields = true,
           };
           //Desserializando para objeto
           var pessoa = JsonSerializer.Deserialize<Pessoa>(json, options);

           Console.WriteLine($"pessoa.Nome: {pessoa.Nome}");
           Console.WriteLine($"pessoa.Email: {pessoa.Email}");
           Console.WriteLine($"pessoa.Nome {pessoa.Cpf}");
           //Serializando
           var pessoaJson =
               JsonSerializer.Serialize<Pessoa>(pessoa,options);
           Console.WriteLine($"JSON de saída: {pessoaJson}");

   //Desserializando para objeto pessoa2
           var pessoa2 = JsonSerializer.Deserialize<Pessoa2>(json);          
           Console.WriteLine($"pessoa2.Nome: {pessoa2.Nome}");
           Console.WriteLine($pessoa2.Email: {pessoa2.Email}");
           Console.WriteLine($"pessoa2.Cpf: {pessoa2.Cpf}"); 
           //Serializando o pessoa2   
           var pessoa2Json = JsonSerializer.Serialize<Pessoa2>(pessoa2);          
           Console.WriteLine($"JSON de saída pessoa2: {pessoa2Json}");
       }
   }

Com a configuração do JsonSerializerOptions ou da inclusão do [JsonInclude], podemos definir quais classes ou campos serão serializados como Json. Como saída da execução do código anterior temos:

JSON de entrada: {"Nome":"Andre Silva","Email":"[email protected]","Cpf":"123456-99"} 
pessoa.Nome: Andre Silva
pessoa.Email: [email protected]
pessoa.Nome 123456-99
JSON de saída: {"Nome":"Andre Silva","Email":"[email protected]","Cpf":"123456-99"}
pessoa2.Nome: Andre Silva
pessoa2.Email: [email protected]
pessoa2.Cpf: 123456-99
JSON de saída pessoa2: {"Nome":"Andre Silva","Email":"[email protected]","Cpf":"123456-99"}
  • Ignorar propriedades condicionalmente: No processo de serialização de objetos, o padrão é pegar todas as propriedades públicas, porém na adição de recursos ao namespace System.Text.Json temos agora a possibilidade de ignorar as propriedades que não devem ser serializadas.
    • Propriedades individuais: usamos o atributo [JsonIgnore]. Exemplo:
public class Usuario
   {
       public string Nome { get; set; }
       public string Email { get; set; }
       [JsonIgnore]
       public string Login { get; set; }
   }

O Json de saída gerado será {"Nome":"Andre Silva","Email":"[email protected]"}. Para adicionar condições usamos uma estrutura igual a apresentada a seguir:

JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string Login { get; set; }

Para saber quais as condições estão disponíveis, consulte a definição da Enumeração disponível no link oficial da documentação.

  • Propriedades somente leitura: Para ignorar as propriedades somente leitura temos a propriedade JsonSerializerOptions.IgnoreReadOnlyProperties, vamos ao exemplo:
public class Usuario
   {
       public string Nome { get; set; }
       public string Email { get; set; }
       public string Login { get; private set; } ="andre.silva"; //propriedade somente leitura e com valor default.
   }
   class Program
   {
       static void Main(string[] args)
       {
           var options = new JsonSerializerOptions
           {
               IgnoreReadOnlyProperties = true,
               WriteIndented = true
           };
           Usuario usuario = new(){
               Nome="Andre Silva",
               Email="[email protected]"    

           };
           var usuarioJson =
              JsonSerializer.Serialize<Usuario>(usuario,options);
          Console.WriteLine($"JSON de saída: {usuarioJson}");

       }
   }

Como saída, temos um Json que vai ignorar a propriedade Login definida como somente leitura, {"Nome":"Andre Silva","Email":"[email protected]"}.

  • Propriedades com valor null: Para ignorar as propriedades de valor nulo, usamos a propriedade DefaultIgnoreCondition, veja o exemplo:
public class Usuario
   {
       public string Nome { get; set; }
       public string Email { get; set; }
       public string Login { get; set; }
   }
   class Program
   {
       static void Main(string[] args)
       {
           JsonSerializerOptions options = new()
           {
               DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
           };

           Usuario usuario = new(){
               Nome="Andre Silva",
               Email=null,
               Login="andre.silva"

           };
           var usuarioJson =
              JsonSerializer.Serialize<Usuario>(usuario,options);
          Console.WriteLine($"JSON de saída: {usuarioJson}");

       }
   }

Como saída teremos o Json {"Nome":"Andre Silva","Login":"andre.silva"}, ignorando a propriedade de valor null.

Tecnologias descontinuadas ou sem suporte no .NET 5

Das tecnologias sem suporte no .NET 5 temos o Web Forms, usado para desenvolver soluções web como se desenvolve para desktop. Para o desenvolvimento web, há algum tempo a Microsoft já recomenda a utilização do ASP. NET MVC, por oferecer maior performance e flexibilidade no uso de diferentes ambientes para desenvolver.

O .NET 5 também não conta mais com o WCF (WIndows Communication Foundation), que definia um modelo para a criação de aplicativos orientados a serviços, com foco nas aplicações distribuídas pela plataforma Windows.

Conclusão

O .NET 5 nasce como um avanço deste produto da Microsoft, com o objetivo de unificar a plataforma .NET como um todo. Com este artigo apresentamos algumas das mudanças que vieram com ele. E aí? O que achou das novidades? Essas opções já estão disponíveis para evoluirmos nossas aplicações baseadas em .NET e criarmos novas soluções multiplataformas.

Agora é esperar o .NET 6. Até a próxima! 😉

Para aprender sobre .NET/C# e mergulhar mais fundo em tecnologia, veja:

André Bessa
André Bessa

Dev C# com foco em backend e instrutor na Alura, buscando sempre novos conhecimentos.

Veja outros artigos sobre Programação