Posts da Tag: C# - Blog da TreinaWeb

C#

C# – Testando requisições com Flurl

No meu artigo passado, demostrei a facilidade do uso da biblioteca Flurl no consumo de dados de uma API. Entretanto o ponto de maior orgulho desta biblioteca é a facilidade em testar as requisições criadas com ela.

Testes de unidades em requisições HTTP

Testar classes que fazem requisições HTTP pode ser um ponto complexo do projeto. Ele precisa ser desenvolvido de forma que aceite um Mock ou fazer uso de uma API de testes.

Uma API de testes pode ser uma boa escolha, mas caso ocorra algo com a mesma, os testes do projeto irão falhar. Enquanto o problema com a API não for resolvido, estes testes não poderão ser realizados.

Então, o mais comum é o uso de Mock, que deve ser implementado de acordo com a biblioteca de requisições implementada, já que a classe HttpClient não pode ser “mockada”. Ela não define interfaces que facilitariam este processo.

Dentre as bibliotecas de requisição que permitem o uso de Mock, a Flurl é a que torna este processo o mais transparente e simples.

Definindo testes de unidade com Flurl

Para exemplificar, serão implementados testes de unidade no projeto apresentado no meu artigo anterior. Nele vimos a implementação do Flurl na camada repository do projeto, assim, os testes de unidade mostrados aqui serão realizados apenas nesta camada.

Os testes podem ser feito graças a classe HttpTest da biblioteca. Recomenda-se que ela seja declarada em um bloco using:

using (var httpTest = new HttpTest()) {
    // Realizar as requisições aqui
}

Desta forma, todas as requisições realizadas dentro deste bloco serão interceptadas pela classe e com isso, poderão ser “mockadas”:

public async Task TestListProducts()
{
    var repository = new ProductRepository();
    using (var httpTest = new HttpTest()) {
        // arrange
        httpTest.RespondWith("[{\"id\":\"12d3d23\",\"name\":\"Mouse\", \"quantity\":10, \"price\": 99.9}, {\"id\":\"213drwa3\",\"name\":\"Teclado\", \"quantity\":20, \"price\": 149.9}]");

        // act
        await repository.FindAll();

        // assert
        httpTest
            .ShouldHaveCalled("http://localhost:3002/api/products")
            .WithVerb(HttpMethod.Get);
    }
}

Acima, é definido o mock:

httpTest.RespondWith("[{\"id\":\"12d3d23\",\"name\":\"Mouse\", \"quantity\":10, \"price\": 99.9}, {\"id\":\"213drwa3\",\"name\":\"Teclado\", \"quantity\":20, \"price\": 149.9}]");

Ou seja, não importa qual requisição, será retornado este conteúdo.

Em seguida a requisição é realizada:

await repository.FindAll();

E é verificado se ela foi feita para a API via GET:

httpTest
    .ShouldHaveCalled("http://localhost:3002/api/products")
    .WithVerb(HttpMethod.Get);

Ao executar o teste, ele irá passar:

asciicast

Definindo critérios para as requisições interceptadas

No exemplo anterior todas as requisições realizadas serão interceptadas pela classe HttpTest. Entretanto, é possível refinar isso:

public async Task TestCreateProducts()
{
    var repository = new ProductRepository();
    using (var httpTest = new HttpTest()) {
        // arrange
        httpTest
                .ForCallsTo("http://localhost:3002/*", "https://api.com/*")
                .WithVerb(HttpMethod.Post)
                .RespondWith("[{\"id\":\"12d3d23\",\"name\":\"Mouse\", \"quantity\":10, \"price\": 99.9}", 201);

        var product = new Product {
            Name = "Mouse",
            Quantity = 10,
            Price = 99.9
        };

        // act
        await repository.Add(product);

        // assert
        httpTest
            .ShouldHaveCalled("*/api/products")
            .WithRequestBody("{\"Id\":*,\"Name\":\"Mouse\",\"Quantity\":10,\"Price\":99.9}")
            .WithVerb(HttpMethod.Post);
    }
}

Note que é definido as APIs que serão interceptadas e o verbo HTTP:

httpTest
        .ForCallsTo("*localhost:3002/*", "*api.*.com/*")
        .WithVerb(HttpMethod.Post)

Qualquer requisição que não se enquadre nestes critérios será ignorada pela HttpTest.

No assert, se verifica apenas o endpoint:

httpTest
    .ShouldHaveCalled("*/api/products")

Não é necessário informar o servidor, porque os aceitos já estão definidos no critério especificado. Também note que é utilizado curingas (*). Eles podem ser implementados nos parâmetros de todos os métodos da classe.

Caso o teste seja executado, este segundo também passará:

asciicast

Conclusão

Adicionar testes em um projeto é uma boa prática que todos devem adotar. Ao se trabalhar com requisições, este processo pode ser facilitado ao adotar a biblioteca Flurl. Assim, caso esteja trabalhando com requisições e necessite implementar testes, não deixe de verificar esta biblioteca.

Neste artigo não foram abordados todos os métodos da classe HttpTest. Como vários são úteis, não deixe de vê-los na documentação da mesma.

Então é isso, por hoje é só 🙂


.NET Core ASP .NET C#

C# – Consumindo APIs com Flurl

Com a diversificação do acesso, está se tornando um padrão a criação de back-end, APIs, que posteriormente serão consumidas por outras aplicações. Por isso, atualmente é imprescindível saber realizar este procedimento. Felizmente, no C#, a biblioteca Flurl facilita, e muito, este processo.

Flurl

Criada por Todd Menier, Flurl é uma biblioteca open source para .NET. Ela se define como um builder de URL, moderno, assíncrono, fluent, portável, testável, entre outras buzzword e uma biblioteca de requisições HTTP.

De forma simples, ela nos permite criar requisições HTTP que são facilmente testáveis. Entretanto, não abordaremos este detalhe aqui. Neste arquivo, veremos apenas a criação de requisições.

Windows Server 2016 - Internet Information Services
Curso de Windows Server 2016 - Internet Information Services
CONHEÇA O CURSO

Aplicação base

Para exemplificar o uso da biblioteca, irei utilizar como base a aplicação demonstrada no artigo da biblioteca RepoDB e o pacote Tw Dev Server do Akira Hanashiro.

Consumindo dados da API

No momento, ao executar a aplicação, os produtos são listados do banco:

Iremos alterá-la pra que esses dados sejam obtidos pela API.

Por esta aplicação adotar o padrão repository, as principais alterações serão realizadas nesta camada. As demais sofrerão apenas adaptações pontuais.

A primeira coisa à ser feita é adicionar a biblioteca Flurl:

dotnet add package Flurl.Http

Por ser uma biblioteca fluent, seus métodos são de extensão. Assim, inicialmente iremos definir a url:

 const string url = "http://localhost:3002/api/products";

Em seguida iremos alterar o método FindAll, que no momento tem o conteúdo abaixo:

public IEnumerable<Product> FindAll()
{
    return QueryAll();
}

A obtenção de dados é feita através de uma requisição GET. Como os dados da nossa API são retornados como JSON, a Flurl possui o método de extensão GetJsonAsync que já cria esta requisição e parseia os dados:

public async Task<IEnumerable<Product>> FindAll()
{
    return await url.GetJsonAsync<List<Product>>();
}

Como GetJsonAsync é assíncrono, note que foi necessário definir o FindAll()como assíncrono. Com isso, será necessário modificar a interface:

Task<IEnumerable<T>> FindAll();

E o controller:

public async Task<ActionResult> Index()
{
    return View((await productRepository.FindAll()).ToList());
}

Também será necessário modificar o model, porque os ids criados pela API são GUID:

public class Product
{
    public string Id { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

Como a API não possui nenhum dado, por enquanto a listagem não mostra nada:

Listagem de produtos com nenhum item

Enviando dados para a API

Com a listagem pronta, vamos enviar dados para a API. Isso é feito via uma requisição POST. Assim, como a GET, a biblioteca possui o método de extensão PostJsonAsync que já facilita a criação desta requisição:

public async Task Add(Product item)
{
    await url.PostJsonAsync(item);
}

Também é necessário modificar a interface:

Task Add(T item);

E o controller:

public async Task<ActionResult> Create([Bind("Id,Name,Quantity,Price")] Product product)
{
    if (ModelState.IsValid)
    {
        await productRepository.Add(product);
        return RedirectToAction("Index");
    }

    return View(product);
}

Agora podemos adicionar dados na API e consumi-los:

Listagem de produtos com um item

Atualizando dados da API

O processo de atualização ocorre em duas etapas. Inicialmente é necessário obter os dados do registro que será atualizado. Para isso, deve ser feita uma requisição GET, passando o id do registro:

public async Task<Product> FindByID(string id)
{
    return await url
                .SetQueryParams(new { id = id })
                .GetJsonAsync<Product>();
}

Note que estamos utilizando o método SetQueryParams passar o id via querystring. No final, teremos uma URL no seguinte formato: http://localhost:3002/api/products?id=<valor id>.

Assim como antes, também é necessário mudar a interface:

Task<T> FindByID(string id);

E o controller:

public async Task<ActionResult> Edit(string id)
{
    if (id == null)
    {
        return StatusCode(StatusCodes.Status400BadRequest);
    }
    Product product = await productRepository.FindByID(id);
    if (product == null)
    {
        return StatusCode(StatusCodes.Status404NotFound);
    }
    return View(product);
}

Aproveite e altere o método de detalhes:

public async Task<ActionResult> Details(string id)
{
    if (id == null)
    {
        return StatusCode(StatusCodes.Status404NotFound);
    }
    Product product = await productRepository.FindByID(id);
    if (product == null)
    {
        return StatusCode(StatusCodes.Status404NotFound);
    }
    return View(product);
}

Agora ao clicar no link de edição (ou detalhes), os dados do produto serão mostrados:

Tela de edição mostrando os detalhes de um produto

A atualização dos dados é realizada via uma requisição PUT e como deve imaginar, a Flurl também fornece um método de extensão que facilita a implementação desta requisição:

public async Task Update(Product item)
{
    await url
            .SetQueryParams(new { id = item.Id })
            .PutJsonAsync(item);
}

Note que também é necessário passar o ID via querystring, pois esta é uma especificação da API.

Não se esqueça de mudar a interface:

Task Update(T item);

E o controller:

public async Task<ActionResult> Edit([Bind("Id,Name,Quantity,Price")] Product product)
{
    if (ModelState.IsValid)
    {
        await productRepository.Update(product);
        return RedirectToAction("Index");
    }
    return View(product);
}

Agora, poderemos alterar os nossos registros:

Listagem de produtos exibindo um produto alterado

Excluindo dados da API

Para finalizar, falta implementar apenas a exclusão dos dados. Isso é feito via uma requisição DELETE, que pode ser implementada via o método de extensão DeleteAsync:

public async Task Remove(string id)
{
    await url
            .SetQueryParams(new { id = id })
            .DeleteAsync();
}

Não se esqueça que é necessário alterar a interface:

Task Remove(string id);

E o controller:

public async Task<ActionResult> DeleteConfirmed(string id)
{
    await productRepository.Remove(id);
    return RedirectToAction("Index");
}

Ao remover o único registro da nossa lista, ela voltará a ficar vazia:

Listagem de produtos com nenhum item

C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

Conclusão

Note que com poucas alterações, conseguimos alterar a fonte de dados da aplicação para uma API. E esta comunicação com a API foi facilitada graças a biblioteca Flurl.

Esta biblioteca fornece uma grande gama de recursos e opções que conheceremos em artigos futuros. Entretanto, caso necessite trabalhar com API no .NET, não deixe de dar uma olhada na sua documentação. Tenho certeza que ela irá facilitar, e muito, o seu trabalho.


.NET Core C#

Serialização JSON com System.Text.Json

A comunicação entre sistemas é algo recorrente no dia a dia do desenvolvedor. Seja a comunicação com um sistema de terceiro, ou mesmo um sistema interno. Entretanto, atualmente o mais comum é a comunicação entre a aplicação e uma API. E nestas situações, geralmente isso ocorre via JSON.

Existem algumas bibliotecas para se trabalhar com JSON no .NET, sendo que as que mais se destacam são Newtonsoft.Json e System.Text.Json. Neste artigo iremos ver a segunda.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Tudo começou no .NET Core 2.1

Conforme o ecossistema do .NET Core foi crescendo, o processamento de dados em JSON se tornou uma parte essencial das aplicações da plataforma, principalmente as voltadas para web, como ASP.NET Core e SignalR. Mesmo que fornecesse alguns recursos para trabalhar com JSON, era quase unanime o uso da biblioteca Newtonsoft.Json para este serviço.

Assim, no .NET Core 2.1 esta biblioteca foi integrada a plataforma e se tornou uma dependência padrão de alguns projetos, como o ASP.NET Core. Algo que foi bem aceito, mesmo que em alguns cenários isso gerasse conflitos e/ou limitações (e.g. não era possível utilizar uma versão da biblioteca não suportada pelo ASP.NET Core 2.1).

Sempre visando melhorar a performance da plataforma, durante o desenvolvimento da versão 3, a equipe da Microsoft notou que a biblioteca Newtonsoft.Json poderia ser um limitador para o processamento de JSON. Não que esta biblioteca não seja performática, mas as suas limitações poderiam ser um gargalo para este aspecto do .NET Core.

A equipe pensou em atualizá-la, mas isso iria “quebrar” as antigas versões da mesma, além de outras bibliotecas que dependem dela. Desta forma, no .NET Core 3.0 foi introduzido uma nova biblioteca JSON, a System.Text.Json.

Visando alta performance, esta biblioteca não é equivalente a Newtonsoft.Json, então não dá para substituir uma pela outra. Em alguns casos, o uso da primeira ainda é recomendado e em caso de dúvidas, a Microsoft fornece uma tabela comparando os recursos de cada uma.

Serialização e Deserialização com System.Text.Json

A biblioteca System.Text.Json fornece três formas de se trabalhar com JSON:

  • JsonSerializer: API de uso geral, utilizada para serializar e desserializar JSON em objetos POCO;
  • JsonDocument: API mais avançada, que permite que o JSON seja quebrado e se torne “navegável”;
  • Utf8JsonReader: API que dá controle total sobre o JSON e permite que o desenvolvedor decida como cada token deve ser tratado.

O uso de cada uma irá depender do objetivo, entretanto, por ser bem mais avançada, neste artigo não iremos abordar o Utf8JsonReader. Veremos às demais a seguir.

JsonSerializer

É bem comum necessitar desserializar um JSON em um objeto. Caso este JSON não tenha nenhuma particularidade, isso pode ser feito de forma simples com o método Deserialize:

using System;
using System.Text.Json;

var json = @"{""Nome"":""Carlos Silva"",""Idade"":33}";

var pessoa = JsonSerializer.Deserialize<Pessoa>(json);
Console.WriteLine(pessoa.Nome);
Console.WriteLine(pessoa.Idade);

public class Pessoa
{
    public String Nome { get; set; }
    public int Idade { get; set; }
}

Se o JSON definir algo fora do padrão, como um valor numérico como string:

var json = @"{""Nome"":""Carlos Silva"",""Idade"": ""33""}";

Pode ser utilizado o JsonSerializerOptions para indicar que este valor deve ser contido para numérico:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

var options = new JsonSerializerOptions
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

var json = @"{""Nome"":""Carlos Silva"",""Idade"": ""33""}";

var pessoa = JsonSerializer.Deserialize<Pessoa>(json, options);
Console.WriteLine(pessoa.Nome);
Console.WriteLine(pessoa.Idade);

public class Pessoa
{
    public String Nome { get; set; }
    public int Idade { get; set; }
}

Se o processo for o inverso, converter um objeto para JSON, pode ser utilizado o método Serialize:

using System;
using System.Text.Json;

var pessoa = new Pessoa { Nome = "Carlos Silva", Idade = 33 };

var json = JsonSerializer.Serialize(pessoa);
Console.WriteLine(json); // {"Nome":"Carlos Silva","Idade":33}

public class Pessoa
{
    public String Nome { get; set; }
    public int Idade { get; set; }
}

Este método também aceita opções:

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

var options = new JsonSerializerOptions
{
    //Lê valores numéricos definidos como string e escreve valores numéricos como strings
    NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString,
    //Identa o JSON gerado
    WriteIndented = true,
    //Ignora propriedades com valor nulo ou padrão
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};

var pessoa = new Pessoa { Nome = "Carlos Silva", Idade = 33 };

var json = JsonSerializer.Serialize(pessoa, options);
Console.WriteLine(json);
// {
//   "Nome": "Carlos Silva",
//   "Idade": "33"
// }

public class Pessoa
{
    public String Nome { get; set; }
    public int Idade { get; set; }
    public List<String> Telefones { get; set; }
}

JsonDocument

O JsonDocument deve ser utilizado para situações onde se possui o JSON, mas não se deseja que seja desserializado para um objeto POCO. Por exemplo, se a intenção for acessar algum elemento dele:

using System;
using System.Text.Json;

var json = @"{""Nome"":""Carlos Silva"",""Idade"":33, ""Telefones"": { ""celular"": ""11-99999-9999"", ""comercial"": ""11-4444-4444""}}";

var obj = JsonDocument.Parse(json);
var nome = obj.RootElement.GetProperty("Nome").GetString();
var celular = obj.RootElement.GetProperty("Telefones").GetProperty("celular").ToString();
Console.WriteLine(nome);//Carlos Silva
Console.WriteLine(celular);//11-99999-9999

Quando se sabe a localização deste elemento, o seu acesso será simples. Entretanto, e se for necessário pesquisar o elemento dentro do JSON? Para estas situações pode ser utilizado os métodos EnumerateObject e/ou EnumerateArray, que permitem percorrer o JSON:

using System;
using System.Linq;
// using System.Collections.Generic;
using System.Text.Json;
// using System.Text.Json.Serialization;

var json = @"{""Nome"":""Carlos Silva"",""Idade"":33, ""Telefones"": { ""celular"": ""11-99999-9999"", ""comercial"": ""11-4444-4444""}}";

var obj = JsonDocument.Parse(json);
var telefones = obj.RootElement.EnumerateObject()
                   .Where(jsonProperty => jsonProperty.Name.Contains("Telefones") 
                                    && jsonProperty.Value.ValueKind == JsonValueKind.Object)
                   .Select(jsonProperty => jsonProperty.Value.GetProperty("comercial"));
foreach(var telefone in telefones)
    Console.WriteLine(telefone);//11-4444-4444

Serialização e desserialização são operações “caras”, que o JsonDocument procura diminuir mantendo a alocação de memória ao mínimo. Desta forma, ele deve ser utilizado, principalmente quando o JSON for muito complexo para uma classe POCO, for necessário acessar apenas algumas partes dele e/ou não se saiba o formato dos seus dados.

C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

Finalizando

Deste que foi lançada a System.Text.Json vem recebendo recursos e melhorias. Ainda não é uma biblioteca substituta para a Newtonsoft.Json (algo que não é a intenção, então não deve ocorrer). Mas devido a sua performance, sempre que ela atender as suas necessidades, opte pelo seu uso.

Então fico por aqui. Até à próxima.


C#

C# 9.0 – Inteiros nativos e inferência de tipos

Finalizando a nossa série de posts sobre os novos recursos do C# 9.0. Neste artigo veremos dois novos recursos: inteiros nativos e melhorias na inferência de tipos.

Inteiros nativos

Aplicações que processam muitas operações matemáticas e precisam garantir que essas operações utilizem o máximo que o processador pode fornecer, agora podem fazer uso dos inteiros nativos.

Inteiros nativos são dois novos tipos de dados representados pelas cláusulas nint e nuint. Nos bastidores elas são alias, respectivamente, dos tipos System.IntPtr and System.UIntPtr.

Em tempo de desenvolvimento não há diferença entre os tipos int e nint:

int defaultInt = 55;
nint nativeInt = 55;

Entretanto, durante a execução da aplicação, se o computador for 32 bits, o nint se comportará como um inteiro de 32bits. Se ele for 64bits, o tipo de comportará como um inteiro de 64 bits.

Para garantir que não haverá perda de dados, pode ser utilizado as constantes int.MinValue e int.MaxValue para verificar o limite inferior e superior de valores.

Melhorias na inferência de tipos

Chamado de target typing, a inferência de tipos é o processo onde o compilador infere o tipo de dado pelo contexto da expressão. Por exemplo:

var nome = "Treinaweb";

Na expressão acima, o compilador sabe que nome é uma variável string, pois está sendo atribuído à ela uma string.

Quando este tipo não pode ser inferido, o tipo da variável deve ser especificado:

string nome = null;

Pelo fato da variável nome, na expressão acima, receber o valor null, o seu tipo é especificado.

Antes do C# 9.0, a inferência de tipos era realizada fazendo o uso da cláusula var, limitando seus benefícios as declarações das variáveis/objetos. Agora na nova versão da linguagem, este recurso foi estendido a cláusula new.

Inferência de tipos em “expressões new”

No C#, a sintaxe para declarar um novo objeto é new T(), sendo T o tipo do objeto que está sendo instanciado. No C# 9.0, caso este tipo já estiver sendo especificado, ele pode ser omitido da expressão new:

Pessoa pessoa = new();

O compilador saberá qual construtor invocado baseado no tipo do objeto. Se este construtor receber algum parâmetro, este deve ser informado:

Pessoa pessoa = new("Carlos");

Como o tipo do objeto precisa ser especificado na expressão, não é possível utilizar esta nova cláusula new com a cláusula var:

var pessoa = new("Carlos");//Será gerado um erro

Quando houver sobrecarga de construtor e/ou parâmetros opcionais?

Mesmo omitindo o tipo, o new funciona da mesma forma que antes. Assim, mesmo se a classe declarar uma sobrecarga de construtor:

class Pessoa {
  public string Nome { get; set; }

  public Pessoa() {}

  public Pessoa(string nome) {
    Nome = nome;
  }
}

O compilador saberá qual chamar de acordo com os parâmetros informados:

Pessoa pessoa1 = new();
Pessoa pessoa2 = new("Thomas");

Isso também ocorre se o construtor possuir parâmetros opcionais:

class Pessoa {
  public string Nome { get; set; }
  public string Tratamento { get; set; }

  public Pessoa(string nome = null, string tratamento = null) {
    Nome = nome;
    Tratamento = tratamento;
  }
}
Pessoa pessoa1 = new("Thomas");
Pessoa pessoa2 = new("Carlos", "Sr.");

E também pode-se fazer uso de parâmetros nomeados:

Pessoa pessoa3 = new(tratamento: "Sra.", nome:"Maria");

Porque usar o new se já há o var?

Se nos limitarmos apenas as declarações dos objetos. Uma expressão com o new:

Pessoa pessoa = new("Thomas");

E uma expressão com o var:

var pessoa = new Pessoa("Thomas");

Irão gerar o mesmo código intermediário. Então nessa situação, o uso de um ou de outro irá depender das preferências do desenvolvedor. Nenhuma das duas expressões irá tornar o código mais ou menos performático.

Entretanto, em situações onde o uso do var não é possível e é que a nova expressão new brilha, como na inicialização das propriedades de uma classe:

public class Curso
{
    public Curso()
    {
        Alunos = new();
    }
    public List<Person> Alunos { get; }
}

O código acima poderia ser resumido para:

public class Curso
{
    public List<Person> Alunos { get; } = new();
}

Melhorando ainda mais a sua legibilidade.

Conclusão

A nona versão do C# trouxe uma leva de novos recursos que visam principalmente melhorar a legibilidade do código e facilitar a vida do desenvolvedor. Assim como ocorre sempre, esses recursos serão adotados aos poucos, conforme novos projetos sejam criados e os antigos sejam migrados para esta nova versão da linguagem.

Então, caso não tenha visto os dois primeiros artigos desta série, recomendo que volte e veja como trabalhar com propriedades init e record e programas top-level e os novos recursos do pattern matching.

Por hoje é só. Até a próxima.


C#

C# 9.0 – Propriedades init e record

Durante a .NET Conf deste ano (2020), a versão 5.0 do .NET foi lançada, junto também saiu a versão final C# 9.0. Como em a cada nova versão da linguagem, esta nona trouxe uma série de recursos que visam facilitar a vida do desenvolvedor e melhorar a legibilidade do código.

Neste e nos próximos artigos mostrarei um pouco desses recursos, começando pelas propriedades init e record.

Propriedades de inicialização

Introduzido na versão 3 do C#, inicializadores de objeto é um ótimo recurso que permite que o objeto seja criado de forma simples e clara. Permitindo inicializar todas as propriedades dele em uma única instrução. Um exemplo simples disso seria:

var pessoa = new Pessoa { Nome = "Carlos Silva", Idade = 33 };

Este recurso possibilita que o objeto seja criado sem a definição de um construtor. No caso do exemplo acima, a classe poderia ser declarada da seguinte forma:

public class Pessoa
{
    public string? Nome { get; set; }
    public int Idade { get; set; }
}

Mas uma grande limitação dele é que as propriedades dos objetos precisam ser mutáveis para que funcione. Isso ocorre porque inicialmente é chamado o construtor da classe (sem parâmetros no caso do exemplo) e só depois que os setters das propriedades são chamados.

Para resolver isso na versão 9.0 foi introduzido o conceito de “propriedades init”. Este tipo de propriedade utiliza o acessor init, que pode ser utilizado no lugar do set:

public class Pessoa
{
    public string? Nome { get; init; }
    public int Idade { get; init; }
}

Desta forma, a propriedade só pode ser inicializada utilizando o inicializador de objeto:

var pessoa = new Pessoa { Nome = "Alex Silva", Idade = 33 }; //Ok
pessoa.Idade = 32; //Será gerado um erro

Na prática significa que o estado do objeto não poderá ser alterado após a sua inicialização.

Acessor init com campos readonly

Como com o acessor init as propriedades só podem receber dados durante a incialização do objeto, ele permite a definição de campos readonly:

public class Pessoa
{
    private readonly string nome = "<desconhecido>";
    private readonly int idade = 0;

    public string Nome 
    { 
        get => nome; 
        init => nome = (value ?? throw new ArgumentNullException(nameof(Nome)));
    }
    public int Idade 
    { 
        get => idade; 
        init => idade = (value ?? throw new ArgumentNullException(nameof(Idade)));
    }
}

Desta forma teremos um comportamento parecido com o obtido quando os campos readonly são inicializados no construtor da classe.

Records (registros)

Um conceito base da programação orientada à objetos é que um objeto possui uma forte identidade e encapsula estados mutáveis ao longo da sua vida. Este tipo de conceito é facilmente aplicável no C#, entretanto as vezes você pode desejar o inverso, que um objeto seja imutável e quando isso ocorre o C# tende a atrapalhar esta implementação.

Para resolver este problema, na versão 9 foi introduzido o conceito de “record” (registro). Este novo tipo de objeto pode ser declarado como uma classe normal, bastando substituir a cláusula class por record:

public record Pessoa
{
    public string? Nome { get; init; }
    public int Idade { get; init; }
}

Um “record” ainda é uma classe, mas ao utilizar a cláusula record, o C# irá adicionar aos objetos dela alguns comportamentos de objetos value-type. A classe não deixa de ser um referece-type, mas na prática passa a ser bem parecida com estruturas. Ou seja, os objetos dela passam a serem definidos pelo seu conteúdo e não a sua identidade.

Mesmo que ainda seja possível criar “records” mutáveis, eles foram criados para melhorar o suporte a objetos imutáveis.

Cláusula with

Quando se trabalha com objetos imutáveis e é necessário representar um novo estado, pode ser um pouco trabalhoso criar novos objetos a partir de um objeto existente. Por exemplo, se for necessário alterar uma propriedade de um objeto imutável, deve-se criar um objeto que seja cópia de um existente, mas a única diferença entre eles será a propriedade que foi alterada. Esta técnica é chamada de mutação não destrutiva.

E para facilitar a implementação dela com “records”, também foi introduzido a cláusula with:

var pessoa = new Pessoa { Nome = "Alex Silva", Idade = 33 };
var novaPessoa = pessoa with { Idade = 32 };

Note que a expressão with acima está utilizando a sintaxe dos inicializados de objeto. Desta forma, ela só pode ser utilizada em propriedades que tenham definido o acessor set ou init.

Nos bastidores esta cláusula irá copiar todo o conteúdo do objeto original (pessoa) e alterar o valor das propriedades definidas na expressão with (Idade).

Igualdade

Um dos comportamentos de value-type adicionados aos “records” é o de igualdade. Todos os objetos do C# herdam o método Equals(object) da classe object e isso não é diferente para os “records”. Este método é utilizado quando os objetos estão sendo comparados.

Objetos reference-type, são comparados pela referência; e value-type, são comparados pelos valores. E mesmo que um “record” seja considerado um objeto reference-type, eles são comparados pelos valores:

var pessoa = new Pessoa { Nome = "Alex Silva", Idade = 33 };
var novaPessoa = pessoa with { Idade = 32 };
var outraPessoa = novaPessoa with { Idade = 33 };

Console.WriteLine(Object.ReferenceEquals(pessoa, outraPessoa));//False
Console.WriteLine(Object.Equals(pessoa, outraPessoa));//True

Com isso, se dois “records” possuírem os mesmos valores, eles serão considerados iguais. Este comportamento também irá ocorrer caso aplique os operadores == e !=, já que para manter a consistência, “records” implementam a interface IEquatable<T> e sobrescrevem esses operadores.

Mas é importante lembrar que igualdade e mudança de estado não combinam muito bem. Um problema dos “records” é que a alteração dos dados pode gerar à alteração do código retornado pelo método GetHashCode e se o objeto for salvo em uma tash table, também muda o resultado dela.

Então é necessário ter cuidado quando se altera um “record”.

Herança

Assim como qualquer classe, “record” também podem ser herdados por outros “records”:

public record Funcionario : Pessoa
{
    public int ID;
}

Os “records” filhos tem acesso à todas as propriedades do “record” pai:

var funcionario = new Funcionario { Nome = "José Silva", Idade = 44, ID = 1234 };

Que também podem ser acessadas com a expressão with:

var outroFuncionario = funcionario with { Nome = "Maria Santos", ID = 2345 };

E podem ser comparadas com o operador de igualdade:

Console.WriteLine(funcionario == outroFuncionario);//False

Record posicional

Nos exemplos anteriores todos os nossos objetos “record” foram criados utilizando o inicializador de objetos ou a expressão with, mas também é possível definir um construtor, e até um desconstrutor, no “record”:

public record Pessoa
{
    public string? Nome { get; init; }
    public int Idade { get; init; }
    public Pessoa (string nome, int idade) 
      => (Nome, Idade) = (nome, idade);
    public Descontruct (out string nome, out int idade) 
      => (Nome, Idade) = (nome, idade);
}

Caso o construtor e o desconstrutor definidos receber por parâmetro valores para todas as propriedades do “record”, a sua declaração pode ser encurtada para:

public record Pessoa (string Nome, int Idade);

Nos bastidores o C# se encarregará de criar as propriedades, o construtor e desconstrutor do “record” definido:

var pessoa = new Pessoa("Carlos Silva", 33); //Construtor
var (nome, idade) = pessoa;//Desconstrutor

É importante frisar que as propriedades serão definidas com o acessor init. E caso queira ter mais controle sobre elas, você pode desabilitar a auto geração definindo propriedades com o mesmo nome no bloco do “record”:

public record Pessoa (string Nome, int Idade)
{
    public string? Nome { get; init; } = Nome;
    public int Idade { get; init; } = Idade;
}

Neste caso serão gerados apenas o construtor e desconstrutor.

Este tipo de declaração reduzida também pode ser utilizada durante a herança:

public record Funcionario (string Nome, int Idade, int ID) : Pessoa(Nome, Idade);

Com isso finalizamos este artigo. No próximo abordarei outros recursos do C# 9.0. Até lá!


.NET Core ASP .NET C#

Implementando cache distribuído no ASP.NET Core

No artigo passado vimos como implementar cache de memória em uma aplicação ASP.NET Core. Entretanto, este tipo de cache não fornece bons resultados quando há mais de uma instância da aplicação, por exemplo, quando é publicada em PaaS. Para este cenário pode ser utilizado cache distribuído.

Cache distribuído

Cache distribuído é um cache compartilhado entre as instâncias de uma aplicação. Tipicamente gerenciado por um serviço externo que a aplicação acessa. Este tipo de cache pode melhorar a performance da aplicação, além de facilitar a sua escalabilidade.

Quando se trabalha com cache distribuído, o dado:

  • É coerente (consistente) entre as requisições pelos vários servidores da aplicação;
  • Não se perde se a aplicação reiniciar;
  • Não utiliza a memória local.

No ASP.NET Core o cache distribuído pode ser implementado, utilizando SQL Server, Redis ou serviços de terceiro, como o NCache. Como o NCache é uma biblioteca paga, não irei abordá-la neste artigo. No caso das outras duas formas, começaremos nosso estudo pelo Redis.

O que é o Redis?

O Redis é um banco de dados estruturados em memória, open source, que pode ser utilizado como database, cache e/ou mensageria. O seu uso mais comum é como sistema de cache e é como o utilizaremos neste artigo.

Para que seja utilizado é necessário que o Redis esteja instalado na máquina atual ou em algum servidor. O seu instalador pode ser obtido no site dele. Entretanto, neste artigo farei uso a versão “containeralizada”, via Docker.

Com isso, caso tenha o Docker instalado na sua máquina, o Redis pode ser inicializado com o comando abaixo:

docker run --name local-redis -p 6379:6379 -d redis

Como Docker não é o foco deste artigo, não expliquei o comando acima, nele o importante é a porta definida no host local (6379), pois ela será informada no ASP.NET Core.

Implementando cache distribuído no ASP.NET Core

Assim como o cache de memória, o cache distribuído é implementado via uma interface, a IDistributedCache. Entretanto o ASP.NET não fornece uma implementação padrão para esta interface, cada uma das implementações de cache distribuído são fornecidas via pacotes NuGet.

No caso do Redis, é necessário adicionar na aplicação o pacote Microsoft.Extensions.Caching.StackExchangeRedis:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Em seguida é necessário registrá-lo no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //...

    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = "localhost:6379";
    });
}

Com isso, a implementação da interface IDistributedCache será “injetada” das dependências da aplicação e poderá ser acessada no construtor:

public class ProductsController : Controller
{
    private readonly IDistributedCache _cache;

    public ProductsController(IDistributedCache cache)
    {
        _cache = cache;
    }
}
C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Para ter acesso a um valor do cache podemos utilizar os métodos Get, GetAsync, que retorna o valor como um array de bytes; ou GetString, GetStringAsync, que o retorna como uma string:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    var products = new List<Product>();

    var json = await _cache.GetStringAsync(cacheKey);
    if(json != null)
    {
        products = JsonSerializer.Deserialize<List<Product>>(json);
    }

    return View(products);
}

Já para salvar um valor no cache, também temos quatro métodos: Set e SetAsync, que salva o valor como um array de bytes; e SetString e SetStringAsync que o salva como uma string:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    var products = new List<Product>();

    var json = await _cache.GetStringAsync(cacheKey);
    if(json != null)
    {
        products = JsonSerializer.Deserialize<List<Product>>(json);
    }
    else {
        products = await _context.Products.ToListAsync();
        json = JsonSerializer.Serialize<List<Product>>(products);
        await _cache.SetStringAsync(cacheKey, json);
    }

    return View(products);
}

Caso queira, também é possível remover o item do cache com os métodos Remove e RemoveAsync.

Definindo uma expiração para o cache

Pela nossa configuração atual, podemos correr o risco de ter dados obsoletos no cache, já que não está sendo definido nenhum prazo de expiração dos dados salvos nele. Podemos fazer isso utilizando a classe DistributedCacheEntryOptions:

products = await _context.Products.ToListAsync();
json = JsonSerializer.Serialize<List<Product>>(products);

var options = new DistributedCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromSeconds(20))
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(1));

await _cache.SetStringAsync(cacheKey, json);

Agora o cache poderá ficar inativo até 20 segundos e será removido depois de um minuto. O tempo do “SlidingExpiration” também pode ser reiniciado com os métodos Refresh e RefreshAsync.

Implementando cache distribuído com SQL Server

Caso tenha intenção de utilizar o SQL Server no lugar do Redis como cache distribuído, inicialmente é necessário executar o comando abaixo:

dotnet sql-cache create <conexao-sql-server> <schema> <tabela>

Para criar no banco a tabela que será utilizada para armazenar os dados do cache.

Em seguida, adicione no projeto o pacote Microsoft.Extensions.Caching.SqlServer:

dotnet add package Microsoft.Extensions.Caching.SqlServer

E por fim, configure o cache no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
        //....
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = "<conexao-sql-server>";
        options.SchemaName = <schema>;
        options.TableName = <tabela>;
    });
}

Note que nesta configuração devem ser informados os mesmos dados passados no comando dotnet sql-cache create.

O uso deste tipo de cache não difere do que vimos com o Redis.

Conclusão

Como o cache de memória deve ser evitado em uma aplicação “multi-servidor”, se este for o seu caso, não hesite em implementar o cache distribuído. Se a sua equipe estiver mais familiarizada com o SQL Server, mesmo que a sua performance seja inferior ao Redis, esta é uma boa escolha. Se este não for o caso, procure sempre optar por uma solução “em-memória”, como o Redis e o NCache.


.NET Core ASP .NET C#

Implementando cache de memória no ASP.NET Core

A performance de um site pode ser um fator determinante para o seu sucesso ou fracasso. Mesmo com a velocidade das conexões crescendo a cada ano, também cresce o número de usuários que fazem uso apenas de dispositivos móveis, onde essas velocidades são significativamente menores. Então toda a aplicação web que deseja se destacar precisa fornecer um bom desempenho e uma das formas de obter isso é implementando cache de memória.

Noções básicas de cache

O cache pode melhorar significativamente a performance e a escalabilidade de uma aplicação, reduzindo o processo necessário para gerar conteúdo. Desta forma, ele funciona melhor com conteúdos que não são muito alterados e tem um custo alto de geração.

O cache cria uma cópia do conteúdo que pode ser retornada muito mais rápida que a fonte original, como demonstra o diagrama abaixo:

Demostra o fluxo do cache.

É importante que a aplicação seja criada de uma forma que não dependa apenas do cache.

Cache no ASP.NET Core

O ASP.NET Core fornece suporte à alguns tipos de cache. O mais simples é o cache de memória, onde o conteúdo é salvo na memória do servidor. Neste tipo de cache, caso a aplicação faça uso de mais de um servidor (for uma aplicação distribuída), é importante que o balanceamento de carga esteja definido para que as requisições subsequentes de um usuário sempre sejam encaminhadas para o mesmo servidor. Caso contrário, a aplicação não conseguirá aproveitar o cache de memória.

Outro tipo de cache suportado pelo ASP.NET Core é o cache distribuído. Geralmente gerenciado por uma aplicação externa, este tipo de cache pode ser compartilhado entre várias instâncias da aplicação. Tornando o tipo de cache ideal para aplicações distribuídas.

Em ambos os casos, o cache trabalha com o armazenamento de dados seguinte do padrão chave-valor.

Neste artigo focaremos apenas no cache de memória.

Implementando cache de memória no ASP.NET Core

No ASP.NET Core, o cache de memória é representado pela interface IMemoryCache, que já é “injetada” nas dependências da aplicação automaticamente pelo framework. Assim, podemos ter acesso a ele no construtor da classe:

public class ProductsController : Controller
{
    private readonly IMemoryCache _cache;

    public ProductsController(IMemoryCache cache)
    {
        _cache = cache;
    }
}  

Com acesso ao cache, pode ser utilizado o método TryGetValue para tentar obter uma informação:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    List<Product> products;
    if (!_cache.TryGetValue<List<Product>>(cacheKey, out products))
    {
        products = await _context.Products.ToListAsync();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(10));

        _cache.Set(cacheKey, products, cacheOptions);
    }
    return View(products);
}

Note que se a informação não constar no cache, ela é salva:

_cache.Set(cacheKey, products, cacheOptions);

Durante o salvamento de um dado, pode ser definida algumas opções, no caso acima é definido o “SlidingExpiration”:

var cacheOptions = new MemoryCacheEntryOptions()
           .SetSlidingExpiration(TimeSpan.FromSeconds(10));

Que indica o tempo que o cache pode ficar inativo, antes de ser removido. É importante prestar atenção neste ponto, pois caso o usuário acesse a aplicação dentro deste tempo, ele é renovado, o que pode fazer com que este item nunca seja removido do cache, mostrando para o usuário algo defasado.

Para este caso, uma alternativa é também definir o “AbsoluteExpiration”:

var cacheOptions = new MemoryCacheEntryOptions()
    .SetSlidingExpiration(TimeSpan.FromSeconds(10))
    .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

Que define o tempo máximo que um item pode ser mantido no cache.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Uma alternativa para o TryGetValue é o uso do GetOrCreate:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";

    var products = await _cache.GetOrCreateAsync<List<Product>>(cacheKey, async entry => {

        entry.SlidingExpiration = TimeSpan.FromSeconds(10);
        entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30);
        return await _context.Products.ToListAsync();
    });

    return View(products);
}

Que tentará obter o item do cache e caso não exista, será adicionado nele.

Por fim, ainda pode ser utilizado o método Get que apenas retorna o item do cache, ou nulo, se ele não for encontrado:

public IActionResult Index()
{
    var cacheKey = "Products";

    var products = _cache.Get<List<Product>>(cacheKey);

    return View(products);
}

Limite do cache de memória

O ASP.NET Core não controla o tamanho do cache de memória, mesmo se não houver espaço na memória, ele tentará salvar a informação nela. Desta forma, para evitar este tipo de situação, podemos definir um limite para este cache.

Como a implementação padrão de IMemoryCache não implementa isso, caso queira definir este limite, é necessário fornecer uma implementação:

public class CustomMemoryCache 
{
    public MemoryCache Cache { get; set; }
    public CustomMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 2048
        });
    }
}

Ela pode ser adicionada nas dependências da aplicação:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<CustomMemoryCache>();
}

E obtidas no construtor das classes:

public class ProductsController : Controller
{
    private readonly MemoryCache _cache;

    public ProductsController(CustomMemoryCache cache)
    {
        _cache = cache;
    }
}  

Entretanto, como o ASP.NET Core não faz o controle do tamanho do cache, ao definir um, sempre que um item for salvo nele, é necessário definir o tamanho deste item:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";

    var products = await _cache.GetOrCreateAsync<List<Product>>(cacheKey, async entry => {

        entry.SlidingExpiration = TimeSpan.FromSeconds(10);
        entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30);
        entry.Size = 10;
        return await _context.Products.ToListAsync();
    });

    return View(products);
}

Este tamanho é arbitrário e não define uma unidade de medida, pode representar o tamanho do item em bytes, o número de caracteres de uma string, a quantidade de itens de uma coleção, etc. Por isso é importante que a aplicação utilize apenas uma unidade de medida. Ao atingir o limite, o cache não salvará mais nenhuma informação.

Conclusão

A implementação de cache de memória no ASP.NET Core é um processo simples que pode fornecer muitos benefícios. Desta forma, caso a sua aplicação necessite exibir muitos dados, que sofram poucas alternações é altamente recomendado que ela implemente cache.

Você pode ver a aplicação apresentada neste artigo no meu Github!


.NET Core Amazon C#

Criando e publicando uma função AWS Lambda com o CLI do .NET Core

AWS Lambda é um serviço da AWS que nos permite criar aplicações no modelo serverless. Fornecendo suporte para as versões 2.1 e 3.1 do .NET Core, este serviço disponibiliza uma série de ferramentas que permite a criação, publicação e execução de uma função do AWS Lambda pelo CLI do .NET Core.

Amazon Web Services (AWS) - Lambda - Fundamentos
Curso de Amazon Web Services (AWS) - Lambda - Fundamentos
CONHEÇA O CURSO

.NET Core CLI

A interface de linha de comando do .NET Core é uma ferramenta multiplataforma que disponibiliza nativamente recursos para a criação, desenvolvimento, execução e publicações de aplicações .NET Core. Além disso, também permite que terceiros forneçam pacotes NuGet que expandem seus recursos.

Neste artigo conheceremos os pacotes disponibilizados pela AWS Lambda, que nos permite gerenciar uma função diretamente pela linha de comando desta ferramenta.

Criando uma função AWS Lambda com CLI do .NET Core

Antes de mais nada é importante que tenha o SDK do .NET Core instalado na sua máquina. No momento da criação deste artigo, só há suporte para as versões 2.1 e 3.1 do .NET Core, então é necessário que instale uma dessas versões. Caso já tenha suporte para uma versão superior, opte por ela.

Também é importante que tenha uma conta na AWS e o AWS CLI configurado na sua máquina (esta configuração será utilizada para publicar a função Lambda). Você pode ver como fazer isso, no artigo sobre a instalação e configuração do AWS CLI do Gabriel.

Para criar uma função Lambda com o CLI do .NET Core, é necessário instalar o pacote Amazon.Lambda.Templates, que fornece uma série de templates do AWS Lambda. Esta instalação pode ser realizada com o comando abaixo:

dotnet new --install Amazon.Lambda.Templates

Note que ao instalar o pacote já serão listados os templates do AWS Lambda que foram disponibilizados na ferramenta:

No nosso caso iremos utilizar o template lambda.EmptyFunction, que permite a criação de uma função Lambda vazia:

dotnet new lambda.EmptyFunction --name ExemploFuncaoLambda

Este template irá criar um projeto com duas pastas: src e test; que contém, respectivamente, o projeto da função e o projeto de testes:

Estrutura do projeto da função Lambda e do projeto de teste, exibido no Visual Studio Code

Outro arquivo importante é o aws-lambda-tools-defaults.json, presente no projeto da função. É nele que se deve informar as configurações do AWS Lambda que a função utilizará. Veremos isso em mais detalhes à frente.

Desenvolvendo a função AWS Lambda

Para este artigo criei uma função simples que retorna o nome da rua de acordo com o cep informado:

public class Function
{
    private static readonly HttpClient client = new HttpClient();

    /// <summary>
    /// A simple function that takes a brazilian zip code and return the street name
    /// </summary>
    /// <param name="input"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task<string> FunctionHandlerAsync(string input, ILambdaContext context)
    {
        if(input != null){
            var streamTask = await client.GetStreamAsync($"https://viacep.com.br/ws/{input}/json/");
            var endereco = await JsonSerializer.DeserializeAsync<Endereco>(streamTask);
            return endereco.logradouro;
        }
        return "CEP não informado";
    }
}

Assim como o método Main do C#, a função Lambda pode ou não ser assíncrona. Neste exemplo estamos definindo uma função assíncrona.

Também note o uso da classe Endereco que possui a estrutura abaixo:

public class Endereco
{
    public string logradouro { get; set; }
}

Como o código da função mudou, é necessário alterar o projeto de teste:

public class FunctionTest
{
    [Fact]
    public void TestToUpperFunction()
    {
        var function = new Function();
        var context = new TestLambdaContext();
        var rua = function.FunctionHandlerAsync("01001000", context).Result;

        Assert.Equal("Praça da Sé", rua);
    }
}

Ele pode ser executado para verificar se está tudo certo com a função:

Com a função definida, podemos publicá-la no AWS Lambda.

Publicando a função no AWS Lambda com o CLI do .NET Core

Para publicar uma função no AWS Lambda, a primeira coisa que deve ser feita é configurar o arquivo aws-lambda-tools-defaults.json:

{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile":"default",
  "region" : "sa-east-1",
  "configuration": "Release",
  "framework": "netcoreapp3.1",
  "function-runtime": "dotnetcore3.1",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "ExemploFuncaoLambda::ExemploFuncaoLambda.Function::FunctionHandlerAsync"
}

Neste arquivo, na opção profile, deve ser informado qual credencial configurada com o AWS CLI será utilizada para a publicação da função. Esta credencial precisa ter permissão de criação de funções Lambda. Geralmente se apenas uma credencial estiver configurada, ela receberá o nome de default.

Nas demais opções temos:

  • region: Região do AWS que a função será publicada;
  • configuration: Tipo de configuração da função, pode ser “Debug” ou “Release”;
  • framework: Framework utilizado para compilar a função;
  • function-runtime: Framework utilizado para executar a função;
  • function-memory-size: Limite de memória utilizada pela função em MB. Deve ser informado um múltiplo de 64.
  • function-timeout: Tempo máximo de execução da função em segundos. Tempo máximo, 15 minutos;
  • function-handler: Caminho da função, segundo o padrão: Projeto::Namespace.Classe::Metodo.

Já para publicar a função iremos utilizar a global tool Amazon.Lambda.Tools, que pode ser instalada com o comando abaixo:

dotnet tool install -g Amazon.Lambda.Tools

Após a instalação dela, acesse a pasta do projeto da função e execute o comando:

dotnet lambda deploy-function FuncaoExemplo --function-role role

No atributo --function-role deve ser informado quais permissões a função terá. Isso determinará se ela terá acesso a outros serviços da AWS. Caso não seja informado, as permissões podem ser definidas durante a publicação da função. Como já tenho uma role de teste definida, vou utilizá-la para publicar a função:

Se a publicação não apresentou nenhum erro, podemos testar a nossa função.

BDD - Testes Guiados por Comportamento com Behat PHP
Curso de BDD - Testes Guiados por Comportamento com Behat PHP
CONHEÇA O CURSO

Executando a função no AWS Lambda com o CLI do .NET Core

Com a função publicada no Lambda, ela pode ser invocada pelo CLI do .NET Core utilizando a opção lambda invoke-function, seguido do nome da função. Por exemplo:

dotnet lambda invoke-function FuncaoExemplo --payload "01001000"

Também note o atributo --payload, onde pode ser informado os dados que serão passados para a função. Ao executar este comando, a função será chamada e será exibido no terminal o seu retorno:

Caso queira excluir a sua função, pode ser utilizado o comando abaixo:

dotnet lambda delete-function FuncaoExemplo

Por fim, mesmo que a maioria das opções que vimos aqui estejam disponíveis no AWS CLI, os recursos adicionados no CLI do .NET Core, facilitam e muito o trabalho de quem desenvolve funções Lambda em C#. Se este é o seu caso, também não deixe de dar uma olhada nos demais recursos disponíveis, utilizando o comando dotnet lambda --help.

É isso por hoje. Você pode ver o código completo da função no meu Github.

Até a próxima 🙂


.NET Core ASP .NET C#

Utilizando o hybrid ORM RepoDb em uma aplicação ASP.NET Core

No ambiente .NET, quando uma aplicação necessita persistir dados, geralmente fará uso de algum framework ORM. Se for algo simples tende a optar por um micro-ORM, como o Dapper e se for complexo, a opção é um “full-ORM”, como o Entity Framework. Mas quando a aplicação estiver no meio termo? É neste ponto que entra o hybrid ORM RepoDb.

RepoDb

O RepoDb é um framework ORM open-source que tem por objetivo sanar a brecha que há entre um micro-ORM e um macro-ORM (full-ORM). Fornecendo recursos que permitem o desenvolvedor alterar de forma simples entre operações básicas e avançadas.

Além de fornecer as operações CRUD padrão (criação, leitura, alteração e exclusão), também disponibiliza recursos avançados como: 2nd-Layer Cache, Tracing, Repositories e operações em lote (Batch/Bulk). Permitindo que seja utilizado tanto em um banco de dados simples quanto nos mais complexos.

Criado por Michael Pendon, o RepoDb se define como a melhor alternativa de ORM para o Dapper e o Entity Framework e procura ter a mesma adoção de ambos. Para ajudá-lo nisso, vamos conhecer este framework através de um exemplo.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Criando a aplicação

Como estou utilizando o Visual Studio Code, criarei a aplicação por linha de comando:

dotnet new mvc -n AspNetCoreRepodb

No momento da criação deste artigo, o RepoDb suporta os seguintes banco de dados:

  • SqlServer: RepoDb.SqlServer;
  • SqLite: RepoDb.SqLite;
  • MySql: RepoDb.MySql;
  • PostgreSql: RepoDb.PostgreSql.

Para o exemplo deste artigo irei utilizar o SQLite, desta forma é necessário adicionar a dependência abaixo:

dotnet add package RepoDb.SqLite

Não se esqueça de aplicar o restore no projeto:

dotnet restore

Com isso já podemos começar a nossa configuração do RepoDb.

Criando o model e tabela

Para este exemplo será utilizada a entidade abaixo:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

Como o RepoDb não realiza este procedimento, também é necessário criar a tabela desta entidade:

CREATE TABLE IF NOT EXISTS [Product]
(
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    Name TEXT,
    Quantity INTEGER,
    Price REAL
);

Agora podemos configurar o acesso ao banco.

Configurando o acesso ao banco de dados

Assim como o Dapper e diferente do Entity Framework, o RepoDb não fornece uma classe de configuração, o banco deve ser acessado via SqlConnection. Desta forma, para organizar o nosso código, irei implementar o padrão repository:

public interface IRepository<T>
{
    string ConnectionString { get; }
    void Add(T item);
    void Remove(int id);
    void Update(T item);
    T FindByID(int id);
    IEnumerable<T> FindAll();
}

Esta interface será implementada pela classe ProductRepository:

public class ProductRepository : IRepository<Product>
{
    private string _connectionString;
    public string ConnectionString => _connectionString;

    public ProductRepository(IConfiguration configuration)
    {
        _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
        RepoDb.SqLiteBootstrap.Initialize();
    } 

    public void Add(Product item)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            var id = dbConnection.Insert<Product, int>(item);
        }
    }

    public IEnumerable<Product> FindAll()
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            return dbConnection.ExecuteQuery<Product>("SELECT * FROM Product");
        }
    }

    public Product FindByID(int id)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            return dbConnection.Query<Product>(e => e.Id == id).FirstOrDefault();
        }
    }

    public void Remove(int id)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Delete<Product>(id);
            //Também poderia ser
            // dbConnection.Delete<Product>(e => e.Id = id);
        }
    }

    public void Update(Product item)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Merge(item);
        }
    }
}

Note que no construtor da classe é chamado o bootstrapper do RepoDb para o SQLite:

public ProductRepository(IConfiguration configuration)
{
    _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
    RepoDb.SqLiteBootstrap.Initialize();
} 

Isso é necessário para configurar o DataMapping da biblioteca para este banco de dados.

Caso trabalhe apenas com SQL RAW, como no exemplo do método FindAll:

public IEnumerable<Product> FindAll()
{
    using (var dbConnection = new SQLiteConnection(ConnectionString))
    {
        return dbConnection.ExecuteQuery<Product>("SELECT * FROM Product");
    }
}

Não é necessário utilizar o bootstrapper, pois esta é a forma mais simples de fazer uso da biblioteca, o que o torna muito semelhante ao Dapper. Mas o seu principal poder é visto quando definimos as ações via Fluent:

public void Add(Product item)
{
    using (var dbConnection = new SQLiteConnection(ConnectionString))
    {
        var id = dbConnection.Insert<Product, int>(item);
    }
}

E para o Fluent funcionar, é necessário que o RepoDb seja “inicializado” para o banco de dados em questão.

Além das ações implementadas nesta classe, também é possível realizar uma ação em lote, como a inserção:

using (var dbConnection = new SQLiteConnection(ConnectionString))
{
    dbConnection.InsertAll<Product>(products, batchSize: 100);
}

Note que é informado no parâmetro batchSize a quantidade de registros serão salvos. Após serem salvos, os id gerados serão atribuídos ao itens da lista.

Com o repositório criado, podemos definir o controller e as views para testar a nossa conexão.

Testando o RepoDb

Para testar, criaremos o controller abaixo:

public class ProductController : Controller
{
    private readonly IRepository<Product> productRepository;

    public ProductController(IRepository<Product> repository) 
        => productRepository = repository;

    // GET: Products
    public ActionResult Index()
    {
        return View(productRepository.FindAll().ToList());
    }

    // GET: Products/Details/5
    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        Product product = productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // GET: Products/Create
    public ActionResult Create()
    {
        return View();
    }

    // POST: Products/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind("Id,Name,Quantity,Price")] Product product)
    {
        if (ModelState.IsValid)
        {
            productRepository.Add(product);
            return RedirectToAction("Index");
        }

        return View(product);
    }

    // GET: Products/Edit/5
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status400BadRequest);
        }
        Product product = productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // POST: Products/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind("Id,Name,Quantity,Price")] Product product)
    {
        if (ModelState.IsValid)
        {
            productRepository.Update(product);
            return RedirectToAction("Index");
        }
        return View(product);
    }

    // GET: Products/Delete/5
    public ActionResult Delete(int? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status400BadRequest);
        }
        Product product = productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // POST: Products/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        productRepository.Remove(id);
        return RedirectToAction("Index");
    }
}

Note que o repositório é recebido por parâmetro no construtor:

public ProductController(IRepository<Product> repository) 
    => productRepository = repository;

Por causa disso, vamos adicioná-lo via injeção de dependência:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddTransient<IRepository<Product>, ProductRepository>();
}

Ao definir as views, poderemos ver o sistema funcionando:

Tabela exibido dois produtos: Teclado e Mouse; com os preços 123,40 e 49,9; e as quantidades 10 e 20

Implementando o padrão Repository com o RepoDb

No nosso exemplo acima, o padrão repository foi implementado “manualmente”. Entretanto, o RepoDb já fornece uma implementação deste padrão. Para utilizá-lo basta herdar a classe BaseRepository, onde deve ser informado a entidade e o tipo de conexão.

Para que a nossa aplicação não necessite de muitas alterações, além desta classe, basta manter a interface IRepository que definimos:

public class ProductRepository : BaseRepository<Product, SQLiteConnection>, IRepository<Product>
{
    public ProductRepository(IConfiguration configuration) : base(configuration.GetValue<string>("DBInfo:ConnectionString"))
    {
        RepoDb.SqLiteBootstrap.Initialize();
    } 

    public void Add(Product item)
    {
        Insert<int>(item);
    }

    public IEnumerable<Product> FindAll()
    {
        return QueryAll();
    }

    public Product FindByID(int id)
    {
        return Query(id).FirstOrDefault();
    }

    public void Remove(int id)
    {
        Delete(id);
    }

    public void Update(Product item)
    {
        Update(item);
    }
}

Note que o código da nossa classe ficou mais “limpo”. Caso execute a aplicação novamente, verá que ela continuará funcionando da mesma forma que antes. Desta forma, quando utilizar esta biblioteca, recomendo fazer uso desta classe BaseRepository. Ela não chega no nível da DbContext do Entity Framework, mas assim como ela, facilita o acesso ao banco.

C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

O RepoDb ainda está crescendo, mas é uma clara boa alternativa quando não necessitar de algo robusto como o Entity e não queira algo muito simples como o Dapper. Portanto, recomendo que nestas situações dê uma chance para este framework, você notará as suas vantagens.

Ah, o código desta aplicação pode ser visto no meu Github.

Até a próxima.


C# Tecnologia

Principais IDEs para desenvolvimento C#

O que é uma IDE (Ambiente de Desenvolvimento Integrado)?

IDE ou Integrated Development Environment (Ambiente de Desenvolvimento Integrado) é um software que auxilia no desenvolvimento de aplicações, muito utilizado por desenvolvedores, com o objetivo de facilitar diversos processos (ligados ao desenvolvimento), que combinam ferramentas comuns em uma única interface gráfica do usuário (GUI). Neste artigo veremos as principais IDEs para desenvolvimento C#.

No artigo “O que é uma IDE”, exploramos algumas características, vantagens e desvantagens em sua utilização. Em outras palavras, podemos dizer que, para o desenvolvedor, é uma forma de criar aplicações de maneira mais rápida, uma vez que estas IDEs auxiliam em todo o processo de desenvolvimento de uma aplicação, provendo diversos benefícios, como a análise de todo o código a ser escrito para identificar bugs causados por um erro de digitação, autocompletam trechos de códigos, e etc.

Abaixo veremos as principais IDEs para desenvolvimento C#.

C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

Principais IDEs para desenvolvimento C

Visual Studio

Logo Visual Studio

Lançado em 1997 pela Microsoft, o Visual Studio é a principal IDE para desenvolvimento C# e todos os seus frameworks, como o .NET e ASP.NET. Além disso, o Visual Studio possui suporte nativo a outras linguagens, como Visual Basic, C, C++ e F#, o tornando ainda mais completo.

Suportado pelo Windows e macOS, é uma das IDEs para desenvolvimento C# rica de funcionalidades que facilitam a implementação de aplicações. Além disso, o Visual Studio provê diversos recursos para ajudar o desenvolvedor, como podemos ver abaixo:

  • Análise de código;
  • Suporta diversos frameworks como .NET, ASP.NET, Unity, Xaramin, dentre outros;
  • Suporte nativo ao .NET Core e ao Azure, serviço de cloud da Microsoft e para o VCS;
  • Detém de suporte a testes unitários integrado;
  • Permite executar queries de bancos de dados SQL;
  • Preenchimento de código inteligente;
  • Verificação dinâmica de erros, entre outros.
C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

O Visual Studio conta também com desenvolvimento multitecnologias, onde, além do C#, oferece suporte para Python, Django, Flask, Node.js, React, Unity e diversas outras tecnologias, o que a torna ainda mais utilizada.

O download do Visual Studio é feito em seu próprio site, onde é possível acompanhar todas as suas novidades, recursos, suporte e muito mais.

Jetbrains Rider

Logo Jetbrains Rider

Lançada em 2017 pela Jetbrains, o Rider é uma IDE para desenvolvimento em C# e toda o seu ecossistema, permitindo a criação de aplicações .NET, jogos com Unity, aplicativos móveis com Xamarin e aplicações web com ASP .NET e ASP .NET Core. Multiplataforma, é possível realizar seu download em diferentes sistemas operacionais como windows, linux e macOS.

Um dos principais concorrentes do Visual Studio, o Rider possui inúmeros recursos, o que facilita a adoção e o uso da IDE em projetos C#. Alguns desses recursos podem ser vistos abaixo:

  • Dispõe de suporte nativo ao Unity para desenvolvimento de jogos;
  • Pode-se desenvolver utilizando tecnologias web, como JavaScript, TypeScript, HTML, CSS e Sass;
  • Suporte à uma ampla variedade de plugins desenvolvidos para o IntelliJ, o tornando ainda mais completo;
  • Navegação e busca de arquivos e trechos de código no projeto;
  • Permite executar queries de bancos de dados SQL;
  • Diferente do Visual Studio, pode ser executado no Windows, Linux e macOS, dentre outros.

O Rider é uma excelente IDE, muito utilizada no mercado. Seu uso facilita a criação de aplicações C#. O download do Rider poderá ser realizado em seu próprio site.

C# (C Sharp) Avançado
Curso de C# (C Sharp) Avançado
CONHEÇA O CURSO

Visual Studio Code

Visual Studio Code

Apesar de ser um editor de textos para desenvolvedores, o Visual Studio Code (ou vscode), é tão completo que é frequentemente confundido como uma IDE. Criada pela Microsoft, o vscode é um editor open source multiplataforma e com diversos recursos para o desenvolvimento C#.

Possui suporte nativo ao JavaScript, TypeScript, JSON, HTML, CSS e outras tecnologias, além disso, é possível instalar plugins para melhorar o suporte para outras tecnologias, como o próprio C#.

Muito utilizado na comunidade, o VScode, apesar de não ser uma IDE, é tão poderosa quanto.Para instalar o vscode, é só acessar sua página oficial e realizar seu download.


.NET Core ASP .NET C# Padrões de Projeto

Mediator Pattern com MediatR no ASP.NET Core

Para facilitar o desenvolvimento, a manutenção e manter o código limpo e legível, grandes aplicações procuram seguir princípios SOLID, padrões de projetos e outras recomendações de boas práticas, como a desacoplação dos objetos. Neste grupo de recomendações, vem ganhando espaço a adoção do Mediator Pattern, que no ASP.NET pode ser facilmente implementado com a biblioteca MediatR, como veremos neste artigo.

C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

O que é Mediator Pattern?

Mediator é um padrão de projetos comportamental especificado pela GoF, que na sua definição formal é descrito como:

Um objeto que encapsula a forma como um conjunto de objetos interage. O Mediator promove o acoplamento fraco ao evitar que os objetos se refiram uns aos outros explicitamente e permite variar suas interações independentemente.

Em outras palavras podemos dizer que o Mediator Pattern gerencia as interações de diferentes objetos. Através de uma classe mediadora, centraliza todas as interações entre os objetos, visando diminuir o acoplamento e a dependência entre eles. Desta forma, neste padrão, os objetos não conversam diretamente entre eles, toda comunicação precisa passar pela classe mediadora.

Podemos ilustrar seu funcionamento com a imagem abaixo:

Representação Gráfica do Mediator Pattern. Há setas apontando e saindo do objeto Mediator para representar a comunicação.

Se um objeto, por exemplo, Objeto A, quiser se comunicar com outro, Objeto C, terá que passar pelo Mediator. Com isso, cada objeto pode focar apenas na sua responsabilidade e não precisa conhecer a estrutura do outro para realizar a comunicação. Se Objeto C for modificado, Objeto A não precisa tomar conhecimento disso, ou seja, cada objeto trabalha de forma independente e isolada.

Entretanto, se houver um grande fluxo de comunicação entre os objetos, o objeto mediador pode se tornar um gargalo para a aplicação. Para resolver isso, a solução comum é implementar o CQRS (Command Query Responsibility Segregation), em tradução livre, Segregação de Responsabilidade de Comando e Consulta.

CQRS vem ao resgate

O Command Query Responsibility Segregation, ou CQRS, é um padrão de projeto que separa as operações de leitura e escrita da base de dados em dois modelos: queries e commands. Os commands são responsáveis pelas ações que realizam alguma alteração na base de dados. Geralmente operações assíncronas que não retornam nenhum dado.

Já as queries são responsáveis pelas consultas, retornando objetos DTOs (Data Transfer Object) para que seja isolada do domínio.

Por gerarem o maior fluxo de informações da aplicação, as consultas podem ser otimizadas através de um serviço específico, como um servidor de cache. Do lado dos commands, o Mediator pode ser utilizado para gerenciar a comunicação dos objetos.

É por causa disso que a biblioteca MediatR implementa conceitos do CQRS, porém não vou me aprofundar nele, já que não é objetivo desse artigo.

O MediatR entra em cena

O MediatR é uma biblioteca open source criada por Jimmy Bogard, o mesmo criador da biblioteca AutoMapper. O seu objetivo é facilitar a implementação do Mediator Pattern em aplicações .NET. Com ela não precisamos nos preocupar com a criação da classe mediadora, pois já são fornecidas interfaces que facilitam a implementação do fluxo de comunicação entre os objetos.

Colocando a mão na massa

Agora que conhecemos os conceitos por trás da biblioteca MediatR, vamos para um exemplo de implementação em um projeto. Será o projeto de uma API simples com um CRUD de pessoas.

A primeira coisa a ser feita é a criação do projeto:

dotnet new webapi -n MediatRSample

Em seguida, iremos adicionar a biblioteca:

dotnet add package MediatR

Para uma aplicação ASP.NET Core, também é necessário instalar o pacote de injeção de dependência:

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Com o projeto criado e as dependências adicionadas, vamos adicionar algumas pastas nele, para que o nosso código fique mais organizado. O projeto ficará com a seguinte estrutura:

Onde dentro da pasta Application, temos as pastas:

  • Commands: Onde serão definidos os objetos DTOs que representam uma ação a ser executada;
  • EventHandlers: Onde serão definidos objetos responsáveis por receber uma notificação gerada pelos Handlers;
  • Handlers: Onde serão definidos objetos responsáveis por receber as ações definidas pelos Commands;
  • Models: Onde serão definidas as entidades utilizadas pela aplicação;
  • Notifications: Onde serão definidos os objetos DTOs que representam notificações.

    Vamos implementar as classes dessas pastas.

Windows Server 2016 - Internet Information Services
Curso de Windows Server 2016 - Internet Information Services
CONHEÇA O CURSO

Especificando a classe domínio e o repositório da aplicação

Antes de implementar os objetos que farão uso da biblioteca MediatR, é necessário especificar a nossa classe de domínio, o model Pessoa:

public class Pessoa
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}

Esta classe contém todas as propriedades que necessitamos para representar um objeto pessoa na nossa aplicação. Os commands que iremos definir serão baseados nela.

Por se tratar de um exemplo simples, esta aplicação não fará uso de um banco de dados, mas possuirá uma interface IRepository:

public interface IRepository<T>
{
    Task<IEnumerable<T>> GetAll();

    Task<T> Get(int id);

    Task Add(T item);

    Task Edit(T item);

    Task Delete(int id);
}

E uma classe repositório que salva os dados em uma coleção estática:

public class PessoaRepository : IRepository<Pessoa>
{
    private static Dictionary<int, Pessoa> pessoas = new Dictionary<int, Pessoa>();

    public async Task<IEnumerable<Pessoa>> GetAll(){
        return await Task.Run(() => pessoas.Values.ToList());
    }

    public async Task<Pessoa> Get(int id){
        return await Task.Run(() => pessoas.GetValueOrDefault(id));
    }

    public async Task Add(Pessoa pessoa){
        await Task.Run(() => pessoas.Add(pessoa.Id, pessoa));
    }

    public async Task Edit(Pessoa pessoa){
        await Task.Run(() =>
        {
            pessoas.Remove(pessoa.Id);
            pessoas.Add(pessoa.Id, pessoa);
        });
    }

    public async Task Delete(int id){
        await Task.Run(() => pessoas.Remove(id));
    }
}

Implementando o padrão Command

Para gerenciar as interações dos objetos, a biblioteca MediatR implementa o padrão Command. Este padrão específica um objeto que encapsula toda informação necessária para executar uma ação posterior.

É neste ponto que a biblioteca faz uso do CQRS, como explicado anteriormente. Como vimos, no CQRS as operações são separadas em queries e commands. A parte dos commands é uma aplicação do padrão Command, que na implementação do MediatR é composta de dois objetos: Command e Command Handler.

Os objetos Command definem solicitações que irão alterar o estado dos dados e que o sistema precisa realizar. Por ser imperativo e se tratar de uma ação que será executada apenas uma vez (por solicitação) é recomendado que esses objetos sejam nomeados com o verbo no imperativo: “cadastra”, “altera”, etc. Também se recomenda a adição de um tipo, e.g., CadastraPessoaCommand, AlteraPessoaCommand, etc.

Já os objetos Command Handler serão responsáveis por executar as ações definidas pelos objetos Command. É aqui que ficará centralizado grande parte da lógica da aplicação.

Vamos iniciar a nossa implementação deste padrão definindo as classes Command:

public class CadastraPessoaCommand : IRequest<string>
{
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class AlteraPessoaCommand : IRequest<string>
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class ExcluiPessoaCommand : IRequest<string>
{
    public int Id { get; set; }
}

Note que essas são classes POCO simples, que contém apenas os atributos necessários para executar a ação especificada. Todas implementam a interface IRequest da biblioteca MediatR.

Nesta interface genérica se especifica o tipo de dado que será retornado quando o command for processado. Também é atráves do uso desta interface que será possível vincular os commands com as classes Command Handlers. É desta forma que a biblioteca saberá qual objeto deve ser invocado quando uma solicitação for gerada.

As boas práticas recomendam que para cada objeto Command haja um objeto Command Handler, entretanto seria possível implementar um objeto Command Handler para lidar com todos os commands definidos na aplicação.

Para este exemplo vou seguir as boas práticas, então a aplicação conterá as três classes abaixo:

public class CadastraPessoaCommandHandler : IRequestHandler<CadastraPessoaCommand, string>
{
    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;
    public CadastraPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    public async Task<string> Handle(CadastraPessoaCommand request, CancellationToken cancellationToken)
    {
        var pessoa = new Pessoa { Nome = request.Nome, Idade = request.Idade, Sexo = request.Sexo };

        try {
            pessoa = await _repository.Add(pessoa);

            await _mediator.Publish(new PessoaCriadaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo});

            return await Task.FromResult("Pessoa criada com sucesso");
        } catch(Exception ex) {
            await _mediator.Publish(new PessoaCriadaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo });
            await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
            return await Task.FromResult("Ocorreu um erro no momento da criação");
        }

    }
}
public class AlteraPessoaCommandHandler : IRequestHandler<AlteraPessoaCommand, string>
{
    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;
    public AlteraPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    public async Task<string> Handle(AlteraPessoaCommand request, CancellationToken cancellationToken)
    {
        var pessoa = new Pessoa { Id = request.Id, Nome = request.Nome, Idade = request.Idade, Sexo = request.Sexo };

        try {
            await _repository.Edit(pessoa);

            await _mediator.Publish(new PessoaAlteradaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo, IsEfetivado = true});

            return await Task.FromResult("Pessoa alterada com sucesso");
        } catch(Exception ex) {
            await _mediator.Publish(new PessoaAlteradaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo, IsEfetivado = false});
            await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
            return await Task.FromResult("Ocorreu um erro no momento da alteração");
        }

    }
}
namespace MediatRSample.Application.Handlers
{
    public class ExcluiPessoaCommandHandler : IRequestHandler<ExcluiPessoaCommand, string>
    {
        private readonly IMediator _mediator;
        private readonly IRepository<Pessoa> _repository;
        public ExcluiPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
        {
            this._mediator = mediator;
            this._repository = repository;
        }

        public async Task<string> Handle(ExcluiPessoaCommand request, CancellationToken cancellationToken)
        {
            try {
                await _repository.Delete(request.Id);

                await _mediator.Publish(new PessoaExcluidaNotification { Id = request.Id, IsEfetivado = true});

                return await Task.FromResult("Pessoa excluída com sucesso");
            } catch(Exception ex) {
                await _mediator.Publish(new PessoaExcluidaNotification { Id = request.Id, IsEfetivado = false });
                await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
                return await Task.FromResult("Ocorreu um erro no momento da exclusão");
            }

        }
    }
}

Note que todos os command handlers implementam a interface IRequestHandler, nesta interface é especificado uma classe command e o tipo de retorno. Quando esta classe command gerar uma solicitação, o MediatR irá invocar o command handler, chamando o método Handler.

É no método Handler que definimos as instruções que devem ser realizadas para aplicar a solicitação definida pelo command.

Após a solicitação ser atendida, é possível utilizar o método Publish() para emitir uma notificação para todo sistema. Onde o MediatR irá procurar por uma a classe que tenha implementado a interface INotificationHandler<tipo da notificacao> e invocar o método Handler() para processar aquela notificação, como veremos a seguir.

Implementando notificações

Na sua essência as solicitações command não retornam nenhuma informação, assim para ser informado que a solicitação foi finalizada com sucesso, ou não, pode ser implementada notificações.

Como vimos no tópico anterior, no método Handler de uma classe Command Handler, pode ser invocado o método Publish(), passando por parâmetro um objeto notificação. Todos os Event Handlers que estiverem “ouvindo” notificações do tipo do objeto “publicado” serão notificados e poderão processá-lo.

Assim, para implementar as notificações inicialmente é necessário definir os objetos notificação:

public class PessoaCriadaNotification : INotification
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class PessoaAlteradaNotification : INotification
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
    public bool IsEfetivado { get; set; }
}
public class PessoaExcluidaNotification : INotification
{
    public int Id { get; set; }
    public bool IsEfetivado { get; set; }
}
public class ErroNotification : INotification
{
    public string Excecao { get; set; }
    public string PilhaErro { get; set; }
}

Como as classes Commands, as notificações são classes POCO que contêm apenas os dados necessários para processar a informação. Para que a biblioteca reconheça o objeto dessas classes como uma notificação é importante que elas implementem a interface INotification.

Já a nossa classe Notification Handler irá “escutar” todas as notificações, pois todas serão apenas registradas no console:

public class LogEventHandler :
                            INotificationHandler<PessoaCriadaNotification>,
                            INotificationHandler<PessoaAlteradaNotification>,
                            INotificationHandler<PessoaExcluidaNotification>,
                            INotificationHandler<ErroNotification>
{
    public Task Handle(PessoaCriadaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"CRIACAO: '{notification.Id} - {notification.Nome} - {notification.Idade} - {notification.Sexo}'");
        });
    }

    public Task Handle(PessoaAlteradaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"ALTERACAO: '{notification.Id} - {notification.Nome} - {notification.Idade} - {notification.Sexo} - {notification.IsEfetivado}'");
        });
    }

    public Task Handle(PessoaExcluidaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"EXCLUSAO: '{notification.Id} - {notification.IsEfetivado}'");
        });
    }

    public Task Handle(ErroNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"ERRO: '{notification.Excecao} \n {notification.PilhaErro}'");
        });
    }
}

A aplicação poderia definir quantas classes Notification Handlers forem necessárias. Por exemplo, além da classe acima poderia haver uma classe que enviaria um e-mail para o usuário informando que uma pessoa foi cadastrada. Outra que avisaria a equipe de suporte que um erro foi gerado.

Caso uma notificação seja “ouvida” por mais de um Notification Handlers, todos serão invocados quando a notificação for gerada.

Controller e configuração MediatR

Agora que os objetos do MediatR estão criados, podemos definir o nosso controller:

[ApiController]
[Route("[controller]")]
public class PessoaController : ControllerBase
{

    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;

    public PessoaController(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok(await _repository.GetAll());
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        return Ok(await _repository.Get(id));
    }

    [HttpPost]
    public async Task<IActionResult> Post(CadastraPessoaCommand command)
    {
        var response = await _mediator.Send(command);
        return Ok(response);
    }

    [HttpPut]
    public async Task<IActionResult> Put(AlteraPessoaCommand command)
    {
        var response = await _mediator.Send(command);
        return Ok(response);
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        var obj = new ExcluiPessoaCommand { Id = id };
        var result = await _mediator.Send(obj);
        return Ok(result);
    }
}

Note que neste controller estamos “injetando” a interface IMediator, isso foi feito para que seja possível enviar as solicitações dos nossos objetos command com o método Send que esta interface disponibiliza:

var response = await _mediator.Send(command);

Neste contexto, o IMediator será a classe mediadora que através do método Send chama os command handlers que definimos, com base no objeto passado.

Por fim, é necessário adicionar o MediatR como serviço da nossa aplicação no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMediatR(typeof(Startup));
    services.AddSingleton<IRepository<Pessoa>, PessoaRepository>();
}

Também repare que estamos registrando a nossa classe repositório para que ela seja “injetada” no nosso controller e nas classes Command Handlers.

E a mágica acontece

Com a nossa aplicação definida, podemos iniciá-la e testar os endpoints. Note que ao executar uma solicitação:

Tela no Postman, mostrando o exemplo de uma requisição POST para o endpoint "https://localhost:5001/pessoa"

Ela será registrada no console:

Print do terminal do Visual Studio Code mostrando o texto: "CRIACAO: '1 - Carlos - 21 - M'"

Todas as solicitações que realizam alguma alteração nos dados serão registradas:

Print do terminal do Visual Studio Code mostrando logs do endpoint

Você pode ver o código completo da aplicação no repositório dela no meu Github.

C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

No episódio de hoje aprendemos…

Mesmo com o exemplo simples demonstrado neste artigo, é possível notar que o Mediator Pattern nos ajuda a manter os objetos do sistema totalmente isolados e independentes, cada um possuindo apenas a sua responsabilidade.

Com a biblioteca MediatR a implementação deste padrão em uma aplicação ASP.NET Core é facilitada. Mas é importante frisar que este o Mediator não deve ser utilizado em qualquer projeto, dependendo do caso, a “sobrecarga ” necessária para implementá-lo não compensará os benefícios que fornece.

Então como He-Man diria, utilize com sabedoria.


.NET Core ASP .NET C#

Criando um Chat com ASP.NET Core SignalR

No artigo passado, abordei aplicações em tempo real com a biblioteca SignalR, onde demonstrei seu uso em uma streaming API.

Como citei no artigo, um uso muito comum desta biblioteca é na criação de chats. Para conhecer mais recursos dela, vamos criar este tipo de aplicação.

Começando pelo começo

Antes de mais nada, criaremos uma aplicação web:

dotnet new web -n ChatSignalR

E nela, iremos criar um Hub:

namespace ChatSignalR.Hubs
{
    public class ChatHub: Hub
    {
        public async Task SendMessage(string usuario, string mensagem)
        {
            await Clients.All.SendAsync("ReceiveMessage", usuario, mensagem);
        }
    }
}

Note que para esta aplicação o Hub possui um método. Será para este método que o cliente enviará mensagens. Assim que a mensagem for recebida, o Hub a enviará para todos os clientes.

Esta será a dinâmica do nosso chat. Um chat aberto, onde todos os usuários conectados receberão todas as mensagens dos demais usuários.

Na parte do servidor, por fim, é necessário habilitar o SignalR e registrar o hub na classe Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services) => services.AddSignalR();

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseDefaultFiles(); 
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ChatHub>("/chat");
        });
    }
}

Criando o cliente:

O nosso cliente será uma página web simples, que conterá um campo para o usuário informar seu nome e uma mensagem:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Mensagens</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" >
</head>
<body>
    <div class="container col-6">
        <div class="form-group">
            <label for="usuario">Usuário</label>
            <input type="text" id="usuario" class="form-control"/>
        </div>
        <div class="form-group">
            <label for="mensagem">Mensagem</label>
            <textarea class="form-control" id="mensagem" rows="2"></textarea>
        </div>
        <input type="button" class="btn btn-primary" id="send" value="Enviar Mensagem" />
    </div>
    <div class="row">
        <div class="col-12">
            <hr />
        </div>
    </div>
    <div class="container col-6">
        <ul class="list-group" id="messagesList"></ul>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/aspnet-signalr/1.1.4/signalr.min.js'></script>
    <script src='main.js'></script>
</body>
</html>

Na parte do JavaScript, conectaremos ao Hub do servidor e definiremos o envio e recebimento de mensagens:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chat").build();
$("#send").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = $("<li></li>").text(user + ": " + msg);
    li.addClass("list-group-item");
    $("#messagesList").append(li);
});

connection.start().then(function () {
    $("#send").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

$("#send").on("click", function (event) {
    var user = $("#usuario").val();
    var message = $("#mensagem").val();
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});

Note que quando uma mensagem for recebida, ela é exibida em uma lista:

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = $("<li></li>").text(user + ": " + msg);
    li.addClass("list-group-item");
    $("#messagesList").append(li);
});

E no envio é indicado o método definido no Hub:

$("#send").on("click", function (event) {
    var user = $("#usuario").val();
    var message = $("#mensagem").val();
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
C# (C Sharp) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
CONHEÇA O CURSO

1,2, 3…. Testando

Agora, ao executar a aplicação e abrir várias abas, note que todos os usuários irão receber as mensagens que forem enviadas:

Simples, não é?

Você pode obter o código da aplicação demonstrado neste artigo no meu Github.