C#

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

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.

    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.

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.

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) Avançado
Curso de C# (C Sharp) Avançado
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.

C# (C Sharp) - ASP.NET MVC
Curso de C# (C Sharp) - ASP.NET MVC
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) Avançado
Curso de C# (C Sharp) Avançado
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.

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.

Publicando uma aplicação .NET Core 3.0 no AWS Elastic Beanstalk

Hoje em dia uma realidade muito comum é utilizar soluções em nuvem para hospedar nossas aplicações. Provedores como a AWS facilitam esse processo oferecendo serviços especializadas para hospedar nossas soluções que se encaixam do modelo de Plataforma como um Serviço (PaaS).

Um desses serviços é o Elastic Beanstalk, que tem suporte nativo a várias linguagens de programação, como Java, Python, NodeJS, PHP, e inclusive .NET Core. Nesse artigo vamos aprender como publicar e configurar um projeto desenvolvido em ASP.NET Core 3.0 no Elastic Beanstalk.

Elastic Beanstalk

O Elastic Beanstalk é um serviço do tipo Plataforma como um Serviço da AWS. Através dele é possível criar ambientes escaláveis e com alta disponibilidade para aplicações web de forma simples e sem muitos conhecimentos aprofundados sobre infraestrutura. Isso o faz uma ótima solução para times pequenos que não tem especialistas em infraestrutura para gerenciar um ambiente na nuvem.

O Elastic Beanstalk suporta inúmeras linguagens, desde Java, NodeJS, PHP, Pyhton, até aplicações desenvolvidas com .NET Core. Recentemente foi adicionado suporte a versão 3.0 do .NET Core e em breve teremos suporte também ao .NET Core 3.1.

Requisitos para criar um projeto .NET Core 3.0

Vamos criar nesse artigo uma nova aplicação .NET Core 3.0 utilizando o Ubuntu 18.04. É preciso que você tenha o .NET Core SDK instalado na sua máquina.

Para instalar o .NET Core SDK, execute os comandos abaixo:

wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb

sudo add-apt-repository universe
sudo apt-get update
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-3.0

Mais detalhes estão disponíveis na documentação oficial da Microsoft.

Criar uma aplicação ASP.NET Core MVC

Para acompanhar a publicação de uma aplicação no Elastic Beanstalk, vamos criar uma nova aplicação em ASP.NET Core MVC simples com o comando abaixo:

dotnet new mvc -o AspNetMvc

Isso irá criar um projeto na pasta AspNetMvc. Por enquanto não vamos alterar nada no projeto criado, em seguida veremos o que precisa ser configurado no seu projeto para publicá-lo no Elastic Beanstalk.

Publicando para o Elastic Beanstalk

O processo de publicação para o Elastic Beanstalk consiste em criar um build do nosso projeto e empacotá-lo num arquivo zip contendo algumas informações adicionais. Felizmente esse processo pode ser automatizado utilizando o AWS Extensions for .NET CLI. Sua instalação é feita como um utilitário global na sua máquina, assim você precisa instalá-lo só na primeira vez.

Para instalar o AWS Extensions for .NET CLI, execute o comando abaixo.

dotnet tool install -g Amazon.ElasticBeanstalk.Tools

Isso permitirá que você utilize o comando dotnet eb para trabalhar com o Elastic Beanstalk diretamente da linha de comando. Caso esse comando não funcione, verifique se o diretório do .NET CLI Tools está adicionado na variável de PATH da sua máquina.

export PATH="$PATH:~/.dotnet/tools"

Antes de efetuar a publicação da sua aplicação, você precisa configurar as credenciais da sua conta da AWS. Caso você tenha o AWS CLI instalado, execute o comando aws configure e preencha com as informações solicitadas:

~/
> aws configure
AWS Access Key ID [None]: ********************
AWS Secret Access Key [None]: ****************************************
Default region name [None]: us-east-1
Default output format [None]: json

Você também pode gerar um arquivo em $HOME/.aws/credentials com os seguintes conteúdo:

[default]
aws_access_key_id = ********************
aws_secret_access_key = ****************************************
Amazon Web Services (AWS) - Fundamentos
Curso de Amazon Web Services (AWS) - Fundamentos
CONHEÇA O CURSO

Por fim, você pode publicar sua aplicação usando o comando dotnet eb deploy-environment. Preencha as perguntas seguintes com o nome da application e environment desejados e, casos eles ainda não existam no Elastic Beanstalk, eles serão criados para você:

~/Code/AspNetMvc (master) ✔
> dotnet eb deploy-environment
Amazon Elastic Beanstalk Tools for .NET Core applications (3.2.0)
Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli

Enter Elastic Beanstalk Application: (The name of the Elastic Beanstalk application.)
aspnetcore-app
Enter Elastic Beanstalk Environment: (The name of the Elastic Beanstalk environment.)
aspnetcore-dev
Enter AWS Region: (The region to connect to AWS services, if not set region will be detected from the environment.)
us-east-1

Para facilitar, você pode criar um arquivo chamado aws-beanstalk-tools-defaults.json com as configurações padrão utilizadas pelo dotnet eb

{
    "region": "us-east-1",
    "application": "aspnetcore-app",
    "environment": "aspnetcore-dev",
    "configuration": "Release"
}

Ao final desse processo, será exibido uma url para acessar o ambiente recém criado. Mas ainda não terminamos, agora vamos aprender como utilizar as configurações de ambiente presentes no Elastic Beanstalk.

Configurações de ambiente

Em uma aplicação .NET Core podemos definir valores dentro de appsettings.json e utilizá-los dentro do nosso código. Como exemplo, podemos adicionar uma configuração chamada Environment que irá conter o nome do nosso ambiente na home page do projeto. Para isso, vamos alterar alguns arquivos do nosso projeto.

No appsettings.json, adicionei a seguinte configuração:

"Environment": "Local"

Na minha view principal exibo essa mensagem:

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<p>@Configuration["Environment"]</p>

Com isso, ao rodar dotnet run essa mensagem será exibida:

Por padrão, o .NET Core também vai carregar as variáveis de ambiente do sistema e seus valores podem sobrescrever o que está definido no appsettings.json. Ao rodar novamente nossa aplicação, mas alterando a variável de ambiente na sua execução, a mensagem será alterada:

~/Code/AspNetMvc
> Environment=Dev dotnet run

Configurações de ambiente no Elastic Beanstalk

Infelizmente o Elastic Beanstalk não faz isso por padrão com o .NET Core, o que pode frustrar alguns desenvolvedores (passei por isso na pele, nesse caso…), pois esse comportamento funciona em outras linguagens e plataformas do Elastic Beanstalk e só no .NET Core isso é diferente. Por ser um problema de longa data, presente desde a primeira versão do .NET Core disponível no Elastic Beanstalk, de acordo com essa pergunta no Stack Overflow, não acredito que esse problema será resolvido em breve.

Para contornar esse problema, podemos customizar o carregamento de configurações da nossa aplicação para ele poder consumir as configurações de ambiente do Elastic Beanstalk.

Instale a dependência TTRider.Aws.Beanstalk.Configuration no seu projeto executando o comando abaixo:

dotnet add package TTRider.Aws.Beanstalk.Configuration

Altere o Program.cs na definição do host builder, adicionando o seguinte método:

.ConfigureAppConfiguration((hostingContext, config) =>
{
    config.AddBeanstalkParameters();
})

No meu caso, o arquivo Program.cs ficou assim:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TTRider.Aws.Beanstalk.Configuration;

namespace AspNetMvc
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddBeanstalkParameters();
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Após feita essas alterações, vamos realizar um novo deploy para o Elastic Beanstalk e alterar as configurações desse ambiente no portal da AWS.

Execute então dotnet eb deploy-environment e adicione uma configuração de ambiente para alterar o valor da mensagem.

Podemos ver abaixo como era o retorno da nossa home no Elastic Beanstalk antes e depois dessa alteração:

Finalizando

Com isso aprendemos como publicar nossa aplicação e como configurar nossa aplicação para utilizar as configurações de ambiente do Elastic Beanstalk. O código completo desse exemplo se encontra disponível no GitHub da TreinaWeb: https://github.com/treinaweb/treinaweb-dot-net-core-aws-beanstalk

Trabalhando com a template engine Liquid no ASP.NET Core

Quando pensamos em template engine para o ASP.NET, a primeira coisa que vem a mente é o Razor. É uma unanimidade o quanto esta template engine é poderosa e, graças ao suporte constante da Microsoft, sempre recebe novos recursos, como os Razor Componentes. Também tem o fato que ela está integrada ao ASP.NET Core, o que facilita o seu uso. Mas ela não é a única opção de template engine para o ASP.NET Core.

Neste quesito se destaca a biblioteca Fluid, que adiciona ao ASP.NET Core suporte a template engine Liquid.

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

Liquid

Liquid é uma template engine open source criada pela empresa Shopify para ser utilizado na criação de temas do seu CMS (sistema de gerenciamento de conteúdo). Possuindo uma sintaxe simples, ele facilita a definição de templates principalmente para quem não está habituado com programação.

Criada em 2006, com o tempo esta template engine passou a ser adotada por outras soluções, principalmente sistemas CMS, como o Orchard Core, que é um framework CMS para ASP.NET Core.

Criando a aplicação que utilizará o Liquid

Diferente do Razor, o Liquid pode ser implementado até em um projeto console (para fazer isso com o Razor é necessário utilizar alguma biblioteca de terceiro). Mas para este artigo, trabalharemos com um projeto web e veremos como substituir o Razor pelo Liquid.

Desta forma, inicialmente iremos criar uma aplicação web vazia:

dotnet new web -n SimpleWebLiquid

Em seguida, adicione no projeto a referência da biblioteca Fluid:

dotnet add package Fluid.MvcViewEngine

Por fim, habilite o Fluid no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services) => services.AddMvc().AddFluid();

Como criamos uma aplicação web vazia, também é importante adicionar as rotas dos controllers no método Configure desta classe:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Pessoa}/{action=Index}/{id?}");
    });
}

Acima é definido como controller padrão Pessoa, pois o criaremos a seguir.

Definindo o modelo e controller do exemplo

Para exemplificar o uso do Liquid, será definido no projeto um model Pessoa:

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

Um repositório simples para ele:

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

    public List<Pessoa> GetAll(){
        return pessoas.Values.ToList();
    }

    public void Add(Pessoa pessoa){
        pessoa.Id = pessoas.Count + 1;
        pessoas.Add(pessoa.Id, pessoa);
    }
}

E por fim, o controller:

public class PessoaController: Controller
{
    public readonly PessoaRepository repository;

    public PessoaController() => repository = new PessoaRepository();

    public IActionResult Index()
    {
        var pessoas = repository.GetAll();

        return View(pessoas);
    }

    public IActionResult Create() => View();

    [HttpPost]
    public IActionResult Create([FromForm] Pessoa pessoa)
    {
        repository.Add(pessoa);
        return RedirectToAction(nameof(Index));
    }
}

Note que este controller não diferente muito de um controller definido quando estamos trabalhando com o Razor. A diferença é em relação ao não uso do validador contra ataques XSRF/CSRF (mesmo sem o Razor, isso pode ser configurado na classe Startup, mas não faz parte do escopo deste artigo) e a definição da anotação [FromForm] no parâmetro da nossa action Create. Como não iremos trabalhar com modelos fortemente tipados nos templates, também não é possível validar os dados do modelo. Isso teria que ser feito “manualmente”.

Criando os templates Liquid

Assim como ocorre com o uso do Razor, o controller também irá procurar pelos templates Liquid das actions dentro de uma pasta com seu nome em Views. Assim, no projeto, crie esta estrutura de pastas:

+-- Views
|   +-- Pessoa

A biblioteca Fluid segue algumas conversões do Razor. Caso haja um arquivo chamado _ViewStart na pasta, ele será o primeiro arquivo carregado. Assim, dentro de Views crie um arquivo chamado _ViewStart.liquid e adicione o conteúdo abaixo:

{% layout '_Layout' %}

A tag {% layout [template] %} é utilizada para definir o template padrão utilizado nas views da pasta atual e todas as subpastas, funcionando como uma “master page”.

Acima, estamos indicando como template um arquivo chamado _Layout, assim o crie dentro da pasta atual (não se esqueça da extensão .liquid) e adicione o conteúdo abaixo:

<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{{ ViewData['Title'] }}</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-sm nnavbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container">
                    Exemplo de Liquid
                </div>
            </nav>
        </header>
        <div class="container">
            <main role="main" class="pb-3">
            {% renderbody %}
            </main>
        </div>  

        <footer class="border-top footer text-muted">
            <div class="container">
                © 2020 - Treinaweb
            </div>
        </footer>
        <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>
    </body>
</html>

No código acima é importante frisar a tag {% renderbody %}. Ela indica o ponto onde o conteúdo das views será renderizado na template. Funcionando como o @RenderBody() do Razor.

Também note o trecho de exibição do título:

<title>{{ ViewData['Title'] }}</title>

Nele estamos utilizando duas chaves. Estas chaves são utilizadas para indicar objetos que devem ser renderizados. Desta forma, quando este template for processado o conteúdo de ViewData['Title'] será exibido no HTML gerado.

Com o nosso template definido, podemos criar dentro da pasta Pessoa, a view de listagem (Index.liquid):

<p>
    <a href="/Pessoa/Create">Adicionar pessoa</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                Id
            </th>
            <th>
                Nome
            </th>
        </tr>
    </thead>
    <tbody>
    {% for p in Model %}
        <tr>
            <td>
                {{ p.Id }}
            </td>
            <td>
                {{ p.Nome }}
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>

E de cadastrado (Create.liquid):

<div class="row">
    <div class="col-md-4">
        <form action="/Pessoa/Create" method="post">
            <div class="form-group">
                <label for="Nome" class="control-label"></label>
                <input name="Nome" id="Nome class="form-control" />
            </div>
            <div class="form-group">
                <input type="submit" value="Salvar" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a href="/Pessoa/Index">Voltar para listagem</a>
</div>

Na view Index, note que estamos utilizando um laço for:

{% for p in Model %}

Este laço percorre um objeto Model. Para que o template reconheça a estrutura deste model, é necessário que ele seja registrado no construtor da classe Startup:

static Startup() => TemplateContext.GlobalMemberAccessStrategy.Register<Pessoa>();

Pronto, agora podemos testar a nossa aplicação.

F# (F Sharp) - Fundamentos
Curso de F# (F Sharp) - Fundamentos
CONHEÇA O CURSO

A mágica acontecendo

Ao executar a aplicação e acessá-la, nos é exibido a tela de listagem:

Também podemos adicionar algumas pessoas:

E notaremos que o nosso template Liquid está funcionando corretamente:

liquid_3

Devo migrar todas as minhas aplicações para o Liquid?

O suporte do Liquid ao ASP.NET não significa que você deve utilizá-lo em todas as suas aplicações. Como dito no início, o Razor é uma poderosa ferramenta, que não deve ser substituída sem necessidade.

O Liquid é uma template engine mais acessível para pessoas com pouco conhecimento em programação. Então quando estiver trabalhando em um projeto onde as pessoas que irão lidar com as templates possuem esta característica, você deve dar uma olhada nesta template engine.

Você pode ver o código completo da aplicação demonstrada neste artigo no meu Github.

ASP.NET Core – Criando uma Streaming API

Quando falamos de streaming de dados, a primeira coisa que vem a cabeça são streaming mídia: áudio e mídia. Mas em algumas situações, pode ser necessário a implementação de streaming de dados textuais.

Utilizado para a entrega de dados em tempo real, o streaming de dados em uma API é uma técnica utilizada principalmente por redes sociais, como Twitter e Facebook.

No .NET Core este tipo de recurso pode ser implementado via SignalR, mas neste artigo exemplificaremos isso utilizando a estrutura tradicional do ASP.NET Core. Futuramente abordarei o uso do SignalR.

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

Criando uma Web API Simples

Para exemplificar a API Streaming, inicialmente iremos criar uma API simples:

dotnet new web -n ApiSimples

Que conterá apenas um model:

namespace ApiSimples.Models
{
    public class Item
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }

        public override string ToString() => $"{Id} - {Name} - {IsComplete}";
    }
}

E um controller:

namespace ApiSimples.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoController : ControllerBase
    {
        private static List<Item> _itens = new List<Item>();

        [HttpGet]
        public ActionResult<List<Item>> Get() => _itens;

        [HttpPost]
        public async Task<ActionResult<Item>> Post([FromBody] Item value)
        {
            if(value == null)
                return BadRequest();

            if(value.Id == 0)
            {
                var max = _itens.Max(i => i.Id);
                value.Id = max+1;
            }

            _itens.Add(value);
            return value;
        }

        [HttpPut("{id}")]
        public async Task<ActionResult<Item>> Put(long id, [FromBody] Item value)
        {
            var item = _itens.SingleOrDefault(i => i.Id == id);
            if(item != null)
            {
                _itens.Remove(item);
                value.Id = id;
                _itens.Add(value);
                return item;
            }

            return BadRequest();
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> Delete(long id)
        {
            var item = _itens.SingleOrDefault(i => i.Id == id);
            if(item != null)
            {
                _itens.Remove(item);
                return Ok(new { Description = "Item removed" });
            }

            return BadRequest();
        }
    }
}

À frente este controller será modificado com adição do streaming.

Não se esqueça de adicionar o suporte para o controller em Startup:

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

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Adicionando o streaming de dados

No ASP.NET Core, ao acessar uma rota, a solicitação é processada e o seu resultado retornado para o usuário. Ao definir um streaming de dados, a solicitação se manterá em um processamento contínuo, retornando dados de um evento, enquanto a conexão se mantiver ativa.

Por não seguir o comportamento padrão, o streaming não pode retornar um objeto ActionResult, que é o tipo de retorno mais comum. Assim, a primeira coisa a ser feita é definir um ActionResult customizado:

public class StreamResult : IActionResult
{
    private readonly CancellationToken _requestAborted;
    private readonly Action<Stream, CancellationToken> _onStreaming;

    public StreamResult(Action<Stream, CancellationToken> onStreaming, CancellationToken requestAborted)
    {
        _requestAborted = requestAborted;
        _onStreaming = onStreaming;
    } 

    public Task ExecuteResultAsync(ActionContext context)
    {
        var stream = context.HttpContext.Response.Body;
        context.HttpContext.Response.GetTypedHeaders().ContentType = new MediaTypeHeaderValue("text/event-stream");
        _onStreaming(stream, _requestAborted);
        return Task.CompletedTask;
    }
}

Note que esta classe implementa a interface IActionResult. Assim, ela poderá ser utilizada no lugar de ActionResult.

O ponto mais importante dela é a definição do callback onStreaming, que é recebido por parâmetro:

public StreamResult(Action<Stream, CancellationToken> onStreaming, CancellationToken requestAborted)
{
  _requestAborted = requestAborted;
  _onStreaming = onStreaming;
} 

Este callback será utilizado para salvar os clients que estiverem “ouvindo” o streaming. E o CancellationToken será utilizado para finalizar o streaming caso a solicitação seja interrompida/cancelada.

Voltando ao controller, é necessário definir uma coleção para salvar os clients:

private static ConcurrentBag<StreamWriter> _clients = new ConcurrentBag<StreamWriter>();

Esta coleção precisa ser thread-safe, por isso que foi utilizada uma coleção do namespace System.Collections.Concurrent.

Agora é possível definir o endpoint do streaming de dados:

[HttpGet]
[Route("streaming")]
public IActionResult Streaming()
{
    return new StreamResult(
        (stream, cancelToken) => {
            var wait = cancelToken.WaitHandle;
            var client = new StreamWriter(stream);
            _clients.Add(client);

            wait.WaitOne();

            StreamWriter ignore;
            _clients.TryTake(out ignore);
        }, 
        HttpContext.RequestAborted);
}

Note que na função callback da classe StreamResult estamos adicionando o client na coleção e aguarda-se que a solicitação seja cancelada. Com isso, ao acessar o endpoint, a solicitação se manterá ativa.

No momento já temos um streaming de dados, mas nada está sendo enviado para os clients. Para isso, definiremos um método que irá percorrê-los e escreverá dados no stream:

private async Task WriteOnStream(Item data, string action)
{
    foreach (var client in _clients)
    {
        string jsonData = string.Format("{0}\n", JsonSerializer.Serialize(new { data, action }));
        await client.WriteAsync(jsonData);
        await client.FlushAsync();
    }
}

Os dados escritos, serão as ações do nosso controller:

public async Task<ActionResult<Item>> Post([FromBody] Item value)
{
    if(value == null)
        return BadRequest();

    if(value.Id == 0)
    {
        var max = _itens.Max(i => i.Id);
        value.Id = max+1;
    }

    _itens.Add(value);

    await WriteOnStream(value, "Item added");

    return value;
}

[HttpPut("{id}")]
public async Task<ActionResult<Item>> Put(long id, [FromBody] Item value)
{
    var item = _itens.SingleOrDefault(i => i.Id == id);
    if(item != null)
    {
        _itens.Remove(item);
        value.Id = id;
        _itens.Add(value);

        await WriteOnStream(value, "Item updated");

        return item;
    }

    return BadRequest();
}

[HttpDelete("{id}")]
public async Task<ActionResult> Delete(long id)
{
    var item = _itens.SingleOrDefault(i => i.Id == id);
    if(item != null)
    {
        _itens.Remove(item);
        await WriteOnStream(item, "Item removed");
        return Ok(new { Description = "Item removed" });
    }

    return BadRequest();
}

Pronto, o nosso streaming de dados já está completo.

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

123 Testando…

Para testar a API, inicialmente é necessário acessar o endpoint do streaming de dados:

Note que o navegador indicará que a página está sendo carregada. Este é o comportamento padrão. Ao enviar uma solicitação para o controller, ela será mostrada nesta página:

A página sempre mostrará que está sendo carregada, mas todas as ações que forem geradas do nosso controller serão exibidas nela.

Simples, não é? Você pode ver esta aplicação completa no meu Github.

No próximo artigo mostrarei como obter isso utilizando o SignalR.

Serviços gRPC no ASP.NET Core

No âmbito de aplicações, quando estamos falando de serviços, o desenvolvedor imagina uma API RESTful. Não dá para negar que o casamento de HTTP+JSON faz um grande sucesso, mas em alguns momentos esta combinação possui limitações.

Em larga escala, ou quando se lida com aplicações críticas, a performance de uma API REST pode significar seu gargalo, principalmente quando estamos lidando com microserviços. É por causa disso que grandes empresas, como Netflix, Digital Ocean, SoundClound e Google, optaram pelo gRPC para melhorar e otimizar a comunicação dos seus microserviços.

O gRPC é um framework RPC open source criado pelo Google, como forma de melhorar a comunicação do grande número de microserviços que possuem. Com suporte à várias linguagens, na versão 3.0 do .NET Core 3.0, a Microsoft adicionou um template que facilita a criação de serviços gRPC.

Antes de conhecer este template e criar um serviço, você precisa conhecer mais sobre o gRPC.

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

O que seria o gRPC?

Como dito, o gRPC é um framework RPC. O RPC é a sigla de Remote Procedure Call, chamada de procedimento remoto, que basicamente trata-se de uma tecnologia para comunicação de processos. Permitindo que um computador possa invocar um procedimento de outro computador, independente da linguagem ou plataforma.

No manifesto de motivação e princípios do gRPC o Google conta que já utilizava uma infraestrutura RPC denominada Stubby. Mas por ser muito associada a infraestrutura da empresa e não ser baseado em nenhum padrão, ela não poderia ser aberta a todos. Com o advento do HTTP/2, o Stubby foi refeito e nasceu o gRPC, que hoje é parte da CNCF, Cloud Native Computer Foundation.

Porque o HTTP/2 é importante?

O gRPC não trabalha sob o protocolo HTTP 1.1, que é o protocolo utilizado por todos os servidores web, devido suas limitações. A principal é a forma que as solicitações são tratadas.

Por mais que não seja aparente, no HTTP 1.1 um servidor só pode receber e responder uma requisição por vez. Para tentar contornar este limite, ele trabalha com paralelismo, mas o canal de comunicação trabalha em apenas uma via. Quando está recebendo não pode enviar e vice-versa.

Já no HTTP/2 as conexões são “multiplexadas” (multiplex), o que significa que trabalha de forma bidirecional, recebendo e enviando várias solicitações ao mesmo tempo.

As duas imagens abaixo ilustram este cenário:

Aqui é importante ressaltar que devido ao uso do HTTP/2, um serviço do gRPC só pode ser hospedado em um servidor que fornece suporte a esta versão do protocolo. Felizmente na versão 3.0 do .NET Core, o Kestrel passou a suportar esta versão.

Para conhecer as características deste tipo de serviço, vamos colocar a mão na massa.

Criando um serviço gRPC no ASP.NET Core

A partir da versão 3.0 do .NET core é possível criar um serviço gRPC através do comando abaixo:

dotnet new grpc -o ServicoGrpc

Ou no Visual Studio 2019, selecione o template gRPC Service.

A aplicação criada terá a estrutura abaixo:

Vamos ver em detalhes seus arquivos.

Contrato com Protobuf

Dentro da pasta Protos temos o arquivo greet.proto:

syntax = "proto3";

option csharp_namespace = "ServicoGrpc";

package Greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

Este é o arquivo de contrato. Baseado no padrão Protobuf, ele que definirá a estrutura do serviço gRPC e precisa existir no cliente e no servidor. É a partir dele que o gRPC irá gerar as operações do serviço.

Acima é definido dentro do pacote Greet um serviço Greeter, que contém uma operação, SayHello. Em seguida é definida a estrutura dos dados recebidos na solicitação e os dados retornados.

Nos bastidores, a biblioteca do gRPC, Grpc.AspNetCore, irá criar classes, que nos permitirá definir o serviço no C#.

Serviços

Os serviços são definidos dentro da pasta Services, que no momento possui apenas a classe GreeterService:

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;
    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}

Esta classe herda Greeter.GreeterBase, que foi gerada pelo gRPC com base no arquivo .proto. O mesmo ocorreu com as classes HelloRequest e HelloReply.

Servidor

Por fim, no arquivo appsettings.json é definido que o Kestrel utilizará o protocolo HTTP/2:

"Kestrel": {
  "EndpointDefaults": {
    "Protocols": "Http2"
  }
}

E na classe Startup, além de registrar o serviço gRPC, cria-se um fallback para caso de o usuário acessar o serviço pelo navegador ou utilizando um client sem suporte ao gRPC:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<GreeterService>();

            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
            });
        });
    }
}

Cliente

Para acessar um serviço gRPC necessitamos de um cliente com suporte a este tipo de serviço, para isso, irei utilizar uma aplicação console simples:

class Program
{
    static async Task Main(string[] args)
    {
        var channel = GrpcChannel.ForAddress("https://localhost:5001");
        var client =  new Greeter.GreeterClient(channel);
        var reply = await client.SayHelloAsync(
                        new HelloRequest { Name = "Treinaweb Blog" });
        Console.WriteLine("Saudacao: " + reply.Message);
        Console.WriteLine("Pressione qualquer coisa para sair...");
        Console.ReadKey();
    }
}

Que precisa referenciar as bibliotecas Google.Protobuf, Grpc.Net.Client e Grpc.Tools:

<ItemGroup>
  <PackageReference Include="Google.Protobuf" Version="3.11.1" />
  <PackageReference Include="Grpc.Net.Client" Version="2.25.0" />
  <PackageReference Include="Grpc.Tools" Version="2.25.0">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>

Além claro do arquivo greet.proto definido do servidor:

<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

No servidor, o atributo GrpcServices é definido com o valor Server.

Ao executar ambas as aplicações, inicialmente o serviço e por fim o cliente, poderemos ver a comunicação sendo realizada:

F# (F Sharp) - Fundamentos
Curso de F# (F Sharp) - Fundamentos
CONHEÇA O CURSO

Conclusão

No momento há poucos servidores web com suporte ao gRPC, mas esta tecnologia possui muito potencial. Em uma arquitetura de microserviços é altamente recomendado que ela seja utilizada para a comunicação entre os microserviços. Neste ambiente a comunicação externa pode se manter com a tradicional combinação HTTP+JSON.

Até a próxima.

Criando uma API RESTful com o Carter e .NET Core

No início deste ano comentei o quanto o ASP.NET Web API é quase uma unanimidade na criação de APIs no .NET, mas que haviam outras opções, tão boas quanto ele.

No artigo em questão abordei a criação de uma API com o framework NancyFX. E ao longo deste ano ganhou destaque outro framework para criação de APIs, que podemos chamar de um derivado do Nancy, o Carter.

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

Carter

O Carter foi criado por Jonathan Channon, e é mantido por ele e a Carter Community, seguindo a mesma filosofia do Nancy (onde o Jonathan Channon também contribui), facilitar a vida do desenvolvedor.

Implementado como uma extensão do ASP.NET Core, o Carter permite que o código seja mais direto, elegante e agradável. Seu principal objetivo é facilitar a definição das rotas e controllers.

Para compreendê-lo, vamos ver um exemplo simples de API.

Criando a aplicação para utilizar o Carter

O Carter pode ser adicionado em qualquer tipo de aplicação ASP.NET Core, mesmo uma aplicação existente. Caso queria trabalhar apenas com este framework é necessário criar uma aplicação web vazia:

dotnet new web -n CarterAPI

E adicionar o pacote carter:

dotnet add package carter

Em seguida ele deve ser configurado na classe Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCarter();
    }

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

        app.UseEndpoints(endpoints => endpoints.MapCarter());
    }
}

Também é possível instalar o CarterTemplate:

dotnet new -i CarterTemplate

Com isso, é possível criar a aplicação utilizando este template:

dotnet new Carter -n CarterAPI

Nesta opção a aplicação é criada com o Carter configurado e um módulo definido. Como não optamos por esta opção, vamos criar o módulo na nossa aplicação.

Criando um módulo

Assim como no Nancy, no Carter os endpoints devem ser definidos em módulos. Um módulo nada mais é que uma classe que herde a classe CarterModule. Esta classe pode ser definida em qualquer ponto da aplicação. O importante é que seja pública, por exemplo:

using Carter;

namespace CarterAPI.Modules
{
    public class HomeModule : CarterModule
    {
        public HomeModule()
        {
            Get("/", (req, res) => res.WriteAsync("Hello from Carter!"));
        }
    }
}

Como é possível supor, acima estamos definindo uma solicitação GET, que será invocada quando o usuário acessar a rota /. Ao executar a aplicação, teremos o resultado:

Para cada verbo do HTTP há um método disponível:

  • Delete = DELETE;
  • Get = GET;
  • Head = HEAD;
  • Options = OPTIONS;
  • Post = POST;
  • Put = PUT;
  • Patch = PATCH.

Criando o CRUD

Para exemplificar uma API REST completa, vamos definir os métodos CRUD.

Inicialmente defina uma entidade:

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

E um repositório:

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

    public List<Pessoa> GetAll(){
        return pessoas.Values.ToList();
    }

    public Pessoa Get(int id){
        return pessoas.GetValueOrDefault(id);
    }

    public void Add(Pessoa pessoa){
        pessoas.Add(pessoa.Id, pessoa);
    }

    public void Edit(Pessoa pessoa){
        pessoas.Remove(pessoa.Id);
        pessoas.Add(pessoa.Id, pessoa);
    }

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

Por fim, podemos definir os endpoints:

public class PessoaModule : CarterModule
{
    public readonly PessoaRepository repository;
    public PessoaModule()
    {
        repository = new PessoaRepository();
        Get("/pessoa/", (req, res) => {
            var pessoas = repository.GetAll();
            return res.AsJson(pessoas);
        });
        Get("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            var pessoa = repository.Get(id);
            return res.Negotiate(pessoa);
        });
        Post("/pessoa/", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            repository.Add(pessoa);
            res.StatusCode = 201;
            await res.Negotiate(pessoa);
            return;
        });
        Put("/pessoa/{id:int}", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            pessoa.Id = req.RouteValues.As<int>("id");

            repository.Edit(pessoa);

            res.StatusCode = 204;
            return;
        });
        Delete("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            repository.Delete(id);
            res.StatusCode = 204;
            return Task.CompletedTask;
        });
    }
}

Nestes endpoints podemos notar algumas particularidades do framework, como o fato dos parâmetros da URL definem um tipo de dado:

Get("/pessoa/{id:int}", (req, res) =>

Isso permite que o valor seja validado no momento que a solicitação está sendo processada. Este valor é obtido através do método do RouteValues:

var id = req.RouteValues.As<int>("id");

Já os dados passados no corpo da solicitação serão obtidos através do Bind:

var pessoa = await req.Bind<Pessoa>();

Podemos testar este endpoint pelo Postman, mas outro recurso do Carter é a geração do JSON definido pelo OpenAPI, que abordei no meu artigo sobre o Swagger.

OpenAPI

O JSON do OpenAPI criado pelo Carter pode ser acessado pela URL /openapi, mas infelizmente ele não configura os endpoints automaticamente. Cada endpoint precisa ser configurado em uma classe que herde RouteMetaData.

Para os endpoints deste artigo, defini as classes abaixo:

public class GetPessoa : RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Retorna uma lista de pessoas";

    public override RouteMetaDataResponse[] Responses { get; } =
    {
        new RouteMetaDataResponse
        {
            Code = 200,
            Description = $"Uma lista de Pessoas",
            Response = typeof(IEnumerable<Pessoa>)
        }
    };

    public override string OperationId { get; } = "Pessoa_GetPessoa";
}
public class GetPessoaById: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Obtém pessoa pelo id";

    public override RouteMetaDataResponse[] Responses { get; } =
    {
        new RouteMetaDataResponse
        {
            Code = 200, 
            Description = $"Uma Pessooa",
            Response = typeof(Pessoa)
        },
        new RouteMetaDataResponse
        {
            Code = 404,
            Description = $"Pessoa não encontrada"
        }
    };

    public override string OperationId { get; } = "Pessoa_GetPessoaById";
}
public class PostPessoa: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Adiciona uma pessoa";

    public override RouteMetaDataRequest[] Requests { get; } =
    {
        new RouteMetaDataRequest
        {
            Request = typeof(Pessoa),
        }
    };

    public override RouteMetaDataResponse[] Responses { get; } = 
    { 
        new RouteMetaDataResponse 
        { 
            Code = 204, 
            Description = "Pessoa adicionada" 
        }
    };
    public override string OperationId { get; } = "Pessoa_PostPessoa";
}
public class PutPessoa: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Atualiza uma pessoa";

    public override RouteMetaDataRequest[] Requests { get; } =
    {
        new RouteMetaDataRequest
        {
            Request = typeof(Pessoa),
        }
    };

    public override RouteMetaDataResponse[] Responses { get; } = 
    { 
        new RouteMetaDataResponse 
        { 
            Code = 204, 
            Description = "Pessoa atualizada" 
        }
    };
    public override string OperationId { get; } = "Pessoa_PutPessoa";
}
public class DeletePessoa: RouteMetaData
{
    public override string Tag { get; } = "Pessoa";
    public override string Description { get; } = "Exclui uma pessoa";

    public override RouteMetaDataResponse[] Responses { get; } = 
    { 
        new RouteMetaDataResponse 
        { 
            Code = 204, 
            Description = "Exclui Pessoa" 
        }
    };

    public override string OperationId { get; } = "Pessoa_DeletePessoa";
}

Agora é necessário definir esses meta dados em cada endpoint:

public class PessoaModule : CarterModule
{
    public readonly PessoaRepository repository;
    public PessoaModule()
    {
        repository = new PessoaRepository();
        Get<GetPessoa>("/pessoa/", (req, res) => {
            var pessoas = repository.GetAll();
            return res.AsJson(pessoas);
        });
        Get<GetPessoaById>("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            var pessoa = repository.Get(id);
            if(pessoa == null)
            {
                res.StatusCode = 404;
                return Task.CompletedTask;
            }
            return res.Negotiate(pessoa);
        });
        Post<PostPessoa>("/pessoa/", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            repository.Add(pessoa);
            Console.WriteLine(pessoa);
            res.StatusCode = 201;
            await res.Negotiate(pessoa);
            return;
        });
        Put<PutPessoa>("/pessoa/{id:int}", async (req, res) =>
        {
            var pessoa = await req.Bind<Pessoa>();

            pessoa.Id = req.RouteValues.As<int>("id");

            repository.Edit(pessoa);

            res.StatusCode = 204;
            return;
        });
        Delete<DeletePessoa>("/pessoa/{id:int}", (req, res) =>
        {
            var id = req.RouteValues.As<int>("id");
            repository.Delete(id);
            res.StatusCode = 204;
            return Task.CompletedTask;
        });
    }
}

Note que eles são especificados no método do endpoint:

Get<GetPessoa>("/pessoa/", (req, res) => {

Ao defini-los e acessar /openapi, será retornado o JSON:

Para visualizarmos em uma interface gráfica, é possível adicionar o SwaggerUi da biblioteca Swashbuckle:

dotnet add package Swashbuckle.AspNetCore.SwaggerUi --version 5.0.0-rc4

Configurá-lo no projeto:

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

    app.UseSwaggerUI(opt =>
    {
        opt.RoutePrefix = "openapi/ui";
        opt.SwaggerEndpoint("/openapi", "Carter OpenAPI Sample");
    });

    app.UseEndpoints(endpoints => endpoints.MapCarter());
}

Desta forma, ao acessar a URL /openapi/ui teremos acesso a interface gráfica:

E por ela é possível testar todos os endpoints.

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

Conclusão

Devido a sua simplicidade e grande gama de recursos o Carter é um framework que merece ser acompanhado. Caso tenha se interessado por ele, recomendo que dê uma olhada no seu repositório.

Infelizmente no momento ele não possui nenhuma documentação detalhada.

Você pode obter os códigos da aplicação demonstrada neste artigo, no meu Github.

Testes unitários no C# com MSTest

Continuando a minha série de artigos sobre as principais bibliotecas de teste do C#, neste artigo abordarei a biblioteca criada e mantida pela Microsoft, a MSTest. Caso queria conhecer a NUnit ou a XUnit, não deixe de ver os artigos anteriores.

Conhecendo a biblioteca MSTest

O Microsoft Test Framework, mais conhecido como MSTest, nasceu como uma ferramenta de testes unitários para aplicações .NET integrada com o Visual Studio. Hoje ela é uma biblioteca separada, que possui recursos poderosos, porém simples e de fácil compreensão.

C# (C Sharp) - TDD
Curso de C# (C Sharp) - TDD
CONHEÇA O CURSO

Criando o projeto que será testado

Assim como nos demais artigos, iremos criar uma solução:

dotnet new sln -n teste-unitario-com-mstest

Criar um projeto de biblioteca de classes:

dotnet new classlib -n Calculos

E criar uma classe, que será testada:

public class Calculadora
{
    public int Soma(int operador1, int operador2) => operador1 + operador2;
    public int Subtracao(int operador1, int operador2) => operador1 - operador2;
    public int Multiplicao(int operador1, int operador2) => operador1 * operador2;
    public int Divisao(int dividendo, int divisor) => dividendo / divisor;
    public (int quociente, int resto) RestoDivisao(int dividendo, int divisor) => (dividendo / divisor, dividendo % divisor);
}

Por fim, é necessário adicionar a referência deste projeto na solução:

dotnet sln teste-unitario-com-mstest.sln add Calculos/Calculos.csproj

Criando o projeto de teste

Sabemos que um projeto de teste se trata de um projeto de biblioteca de classes que contém a referência de uma biblioteca de teste. Felizmente, o .NET fornece um template que já cria um projeto com a referência da biblioteca de teste. Para o MSTest o projeto pode ser criado com o comando abaixo:

dotnet new mstest -n Calculos.Tests

Este projeto será adicionado na solução:

dotnet sln teste-unitario-com-mstest.sln add Calculos.Tests/Calculos.Tests.csproj

E o projeto Calculos será referenciado nele:

dotnet add Calculos.Tests/Calculos.Tests.csproj reference Calculos/Calculos.csproj

Agora podemos conhecer os recursos desta biblioteca.

Adicionando os testes unitários

No projeto de teste, Calculos.Tests exclua o arquivo TestUnit1.cs e adicione um novo arquivo chamado CalculadoraTests.cs, neste arquivo iremos criar uma classe com o mesmo nome, que terá a estrutura abaixo:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Calculos.Tests
{
    [TestClass]
    public class CalculadoraTests
    {
        [TestMethod]
        public void Soma_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Soma(10, 20);
            //Verifica se o resultado é igual a 30
            Assert.AreEqual(30, resultado);
        }
    }
}

Observe que é utilizado o atributo TestClass para indicar que a classe criada é uma classe de teste. Dentro dela, há um método marcado com o atributo TestMethod, que sinaliza que este é um método de teste.

Quando o runner da biblioteca for executado, ele irá analisar dentro do projeto quais são as classes que possuem o atributo TestClass e irá chamar os métodos marcados com o atributo TestMethod.

Diferente de outras bibliotecas, o runner da MSTest não é executado diretamente, ele é chamado quando o teste é executado utilizando o comando dotnet test ou Test Explorer do Visual Studio.

Para este artigo, utilizarei a linha de comando:

Note que o único teste definido passou.

Definindo vários testes unitários de uma só vez

No momento a nossa classe de teste possui apenas um teste unitário, que analisa um grupo específico de dados de entrada. Caso seja necessário analisar vários dados de entrada, pelo que vimos até o momento, seria necessário criar vários testes.

Felizmente o MSTest possui os atributos DataTestMethod e DataRow que permite criar um método de teste, que varia apenas os dados de entrada:

[DataTestMethod]
[DataRow(1)]
[DataRow(2)]
[DataRow(3)]
[DataRow(4)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.AreEqual(0, resultado.resto);
}

Ao executar os testes:

Note que cada dado de entrada informado nos DataRow é considerado um teste a parte e o grupo é considerado outro teste. É por isso que a contagem de testes é 6.

Se o teste não passar com um dos valores:

[DataTestMethod]
[DataRow(1)]
[DataRow(2)]
[DataRow(3)]
[DataRow(5)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.AreEqual(0, resultado.resto);
}

Teremos o resultado:

Ou seja, um dos testes não passou, assim é indicado que o grupo também não passou. Por isso que é indicado dois erros.

Para finalizar, e obter uma cobertura de 100% dos testes, vamos definir método de testes para os outros métodos da nossa classe de Calculadora:

[TestClass]
public class CalculadoraTests
{
    [TestMethod]
    public void Soma_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Soma(10, 20);
        //Verifica se o resultado é igual a 30
        Assert.AreEqual(30, resultado);
    }

    [DataTestMethod]
    [DataRow(1)]
    [DataRow(2)]
    [DataRow(3)]
    [DataRow(4)]
    public void RestoDivisao_DeveRetornarZero(int value)
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(12, value);
        //Verifica se o resto da divisão é 0
        Assert.AreEqual(0, resultado.resto);
    }

    [TestMethod]
    public void RestoDivisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(10, 3);
        //Verifica se o quociente da divisão é 3 e o resto 1
        Assert.AreEqual(3, resultado.quociente);
        Assert.AreEqual(1, resultado.resto);
    }

    [TestMethod]
    public void Subtracao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Subtracao(20, 10);
        //Verifica se o resultado é igual a 10
        Assert.AreEqual(10, resultado);
    }

    [TestMethod]
    public void Divisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Divisao(100, 10);
        //Verifica se o resultado é igual a 10
        Assert.AreEqual(10, resultado);
    }

    [TestMethod]
    public void Multiplicao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Multiplicao(5, 2);
        //Verifica se o resultado é igual a 10
        Assert.AreEqual(10, resultado);
    }
}
C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Demais comparadores do MSTest

Nos exemplos apresentados neste artigo, utilizamos somente o comparador AreEqual. Felizmente a biblioteca fornece uma série de comparadores que podem ser visualizados na documentação dela.

Sendo uma das bibliotecas de testes unitários mais simples do C#, com ela não há desculpa para não adicionar testes unitários nas suas aplicações.

Fico por aqui, até o próximo artigo.

Testes unitários no C# com o xUnit

No meu último artigo comentei sobre o hábito de sempre adicionar testes unitários as aplicações que desenvolvo e sobre o meu desejo de criar uma série de artigos abordando as bibliotecas de testes unitários do C#. Então dando continuidade a este tema, neste artigo abordarei a biblioteca xUnit.

Conhecendo a biblioteca xUnit

O xUnit é uma biblioteca open source de testes unitários, que foi criada pelo mesmo criador da segunda versão do NUnit, James Newkirk. Desta forma, ambas as bibliotecas possuem funcionalidades similares.

Criando o projeto que será testado

Como no artigo anterior, para exemplificar a biblioteca xUnit, iremos criar uma solução:

dotnet new sln -n teste-unitario-com-xunit

E nela adicionar uma biblioteca de classes:

dotnet new classlib -n calculos

Que conterá uma classe Calculadora:

public class Calculadora
{
    public int Soma(int operador1, int operador2) => operador1 + operador2;
    public int Subtracao(int operador1, int operador2) => operador1 - operador2;
    public int Multiplicao(int operador1, int operador2) => operador1 * operador2;
    public int Divisao(int dividento, int divisor) => dividento / divisor;
    public (int quociente, int resto) RestoDivisao(int dividento, int divisor) => (dividento / divisor, dividento % divisor);
}

Adicione a referência deste projeto na solução:

dotnet sln teste-unitario-com-xunit.sln add calculos/calculos.csproj

Criando o projeto de teste

Um projeto de teste trata-se de uma biblioteca de classe que contenha a referência xunit. Felizmente no .NET Core, há a opção de um template de projeto já com esta biblioteca adicionada. Para criar este tipo de projeto utilize o comando abaixo:

dotnet new xunit -n calculos.tests

Adicione este projeto na solução:

dotnet sln teste-unitario-com-xunit.sln add calculos.tests/calculos.tests.csproj

E adicione nele a referência da biblioteca de cálculos nele:

dotnet add calculos.tests/calculos.tests.csproj reference calculos/calculos.csproj

Pronto, agora podemos adicionar testes unitários na nossa aplicação.

Adicionando testes unitários

No projeto de teste criado, remova o arquivo UnitTest1.cs e adicione uma classe chamada CalculadoraTest, nela adicione o método abaixo:

public class CalculadoraTest
{
    [Fact]
    public void Soma_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Soma(10, 20);
        //Verifica se o resultado é igual a 30        
        Assert.AreEqual(30, resultado);
    }
}

Note que no código acima é utilizado o atributo em cima do método Soma_DeveRetornarOValorCorreto. Isso indica que este é um método de teste. É a graças a ele que os tests runner sabem qual método deve ser chamado quando um teste é iniciado.

Este teste pode ser executado através do comando dotnet test, o Test Explorer do Visual Studio ou o xUnit Runner Console.

Com o teste definido, ele pode ser executado:

dotnet test

Onde teremos resultado:

Note que ele indica que o teste definido passou.

C# (C Sharp) - TDD
Curso de C# (C Sharp) - TDD
CONHEÇA O CURSO

Também é possível criar uma “teoria”. Teoria executa o mesmo teste com uma série de parâmetros. Caso algum dos parâmetros gere um resultado inesperado, ela é considerada falha.

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(5)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.Equal(0, resultado.resto);
}

Repare que a teoria é definida com a anotação Theory e que cada parâmetro testado é indicado pela anotação InlineData.

Caso o teste falhe, é indicado com qual valor isso ocorreu:

Para finalizar e para obtermos uma cobertura dos testes de 100%, vamos definir testes para os demais métodos de Calculadora:

public class CalculadoraTest
{
    [Fact]
    public void Soma_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Soma(10, 20);
        //Verifica se o resultado é igual a 30        
        Assert.Equal(30, resultado);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    public void RestoDivisao_DeveRetornarZero(int value)
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(12, value);
        //Verifica se o resto da divisão é 0
        Assert.Equal(0, resultado.resto);
    }

    [Fact]
    public void RestoDivisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(10, 3);
        //Verifica se o quociente da divisão é 3 e o resto 1
        Assert.Equal(3, resultado.quociente);
        Assert.Equal(1, resultado.resto);
    }

    [Fact]
    public void Subtracao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Subtracao(20, 10);
        //Verifica se o resultado é igual a 10
        Assert.Equal(10, resultado);
    }

    [Fact]
    public void Divisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Divisao(100, 10);
        //Verifica se o resultado é igual a 10
        Assert.Equal(10, resultado);
    }

    [Fact]
    public void Multiplicao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Multiplicao(5, 2);
        //Verifica se o resultado é igual a 10
        Assert.Equal(10, resultado);
    }
}

Demais comparadores do xUnit

O exemplo mostrado aqui é simples, nele fazemos uso apenas do comparador Assert.Equal. Para o caso do exemplo apenas este comparador foi necessário. Agora sempre que um método da classe for alterado, os testes podem ser executados para verificar se a alteração gerou algum impacto nos demais métodos dela.

Infelizmente a documentação do xUnit não explica os comparadores que podem ser utilizados com a biblioteca. Eles só são listados na comparação com outras bibliotecas de teste.

Caso seja adicionada alguma referência para eles na documentação, a adicionarei aqui posteriormente.

Por hoje é isso. No meu próximo artigo falarei do MSTest, até lá!

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200