.NET Core

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

Verificando a integridade da aplicação ASP.NET Core com Health Checks

Alguns artigos passados falei sobre as ferramentas de linha de comando que permitem diagnosticar uma aplicação no .NET Core 3.0. Por serem ferramentas de linha de comando, elas requerem acesso a máquina onde a aplicação está instalada. Infelizmente em muitos cenários algo assim não é possível, mas o monitoramento do status da aplicação não deixa de ser importante nesses casos.

Se tratando de aplicações ASP.NET, antigamente o comum era criar um endpoint que retornasse o estado da aplicação ou menos nada era definido. Mas com a adoção cada vez maior de microsserviços e a criação de aplicações para um ambiente distribuído, implementar a verificação da integridade da aplicação se tornou algo essencial.

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

Felizmente, a partir da versão 2.2 do ASP.NET Core, foi introduzido os Health Checks que facilitam este trabalho.

Conhecendo os Health Checks

Health check é um middleware que fornece um endpoint que retorna o status da aplicação. Na sua versão básica, a aplicação é considerada saudável caso retorne o código 200 (OK) para uma solicitação web. Mas também são fornecidas bibliotecas que nos permite verificar o status de serviços utilizados pela aplicação, como: banco de dados, sistema de mensageria, cache, logging, serviços externos ou mesmo a criação de um health check customizado.

Aplicação que terá a integridade verificada

Para exemplificar o uso do health check, vou utilizar uma aplicação ASP.NET Core Web API simples, que já foi mostrada no artigo sobre a documentação de uma ASP.NET Core Web API com o Swagger, que você pode ver no meu Github.

Adicionando o Health Check na aplicação

Agora que já temos a aplicação, já podemos ativar o health check nela. Para isso, basta adicionar o service no método ConfigureServices da classe Startup e indicar a URL dele no método Configure:

public void ConfigureServices(IServiceCollection services)
{
    //....
    services.AddHealthChecks();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...

    //Ativa o HealthChecks
    app.UseHealthChecks("/status");
    //...
}

Agora, ao executar a aplicação e acessar a URL /status será exibido se ela está saudável ou não:

Como está sendo testado apenas se ela responde a uma requisição, a aplicação é considerada saudável.

Integridade do banco de dados

Existem várias formas de testar a integridade do banco de dados. É fornecido uma gama de pacotes que nos permite testar a integridade do banco de acordo com o SGBD:

  • SQL Server: AspNetCore.HealthChecks.SqlServer;
  • MySQL: AspNetCore.HealthChecks.MySql;
  • PostgreSQL: AspNetCore.HealthChecks.Npgsql;
  • SQLite: AspNetCore.HealthChecks.SqLite;
  • Oracle: AspNetCore.HealthChecks.Oracle;
  • MongoDb: AspNetCore.HealthChecks.MongoDb.

Caso esteja trabalhando com o Entity Framework, também há o pacote Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore, que será o que utilizaremos na aplicação:

dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore

Com isso, basta adicionar a classe DbContext, que o estado da conexão com o banco será verificada:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>();
}

Se o banco estiver saudável, o resultado não será alterado:

Caso haja algum problema com a conexão com o banco, a aplicação não será considerada saudável:

Alterando as informações exibidas

No momento, somos informados apenas se a aplicação está ou não saudável. Para uma aplicação pequena, isso pode ser útil, mas imagine uma aplicação que verifica vários serviços? Apenas com esta informação não conseguiremos saber qual serviço não está funcionando corretamente.

Para obter esses dados, podemos customizar as informações de saída do health check, definindo um delegate na opção ResponseWriter:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...

    //Ativa o HealthChecks
    app.UseHealthChecks("/status", new HealthCheckOptions()
    {
        // WriteResponse é um delegate que permite alterar a saída.
        ResponseWriter = (httpContext, result) => {
            httpContext.Response.ContentType = "application/json";

            var json = new JObject(
                new JProperty("status", result.Status.ToString()),
                new JProperty("results", new JObject(result.Entries.Select(pair =>
                    new JProperty(pair.Key, new JObject(
                        new JProperty("status", pair.Value.Status.ToString()),
                        new JProperty("description", pair.Value.Description),
                        new JProperty("data", new JObject(pair.Value.Data.Select(
                            p => new JProperty(p.Key, p.Value))))))))));
            return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented));
        }
    });
    //...
}

Com isso, obteremos a causa do problema da aplicação:

Criando health checks customizados

Existe uma grande gama de health checks definidos, mas em algumas situações, pode ser necessário definir um customizado. Para isso, é necessário definir uma classe que implemente a interface IHealthCheck e definir no método CheckHealthAsync qual tipo de verificação ele fará:

public class SelfHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        return Task.FromResult(new HealthCheckResult(
            HealthStatus.Healthy,
            description: "API up!"));
    }
}

Acima é definido um health check simples, que apenas indica que a API está funcionando.

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

Agora é necessário adicioná-lo ao serviço com o método AddCheck:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>()
            .AddCheck<SelfHealthCheck>("Self");
}

Também pode ser definido um método de extensão:

public static class HealthCheckBuilderExtensions
{
    public static IHealthChecksBuilder AddSelfCheck(this IHealthChecksBuilder builder, string name, HealthStatus? failureStatus = null, IEnumerable<string> tags = null)
    {
        // Register a check of type SelfHealthCheck
        builder.AddCheck<SelfHealthCheck>(name, failureStatus ?? HealthStatus.Degraded, tags);

        return builder;
    }
}

E utilizá-lo para registrar o health check:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>()
            .AddSelfCheck("Self");
}

Ao acessar a aplicação, ele também será mostrado:

Adicionando uma interface

No momento o resultado do status da aplicação é um JSON, mas existe um pacote que nos permite visualizar as informações em uma interface gráfica. Para isso, adicione na aplicação os pacotes AspNetCore.HealthChecks.UI e AspnetCore.HealthChecks.UI.Client:

dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspnetCore.HealthChecks.UI.Client

Em seguida é necessário habilitar o Health Checks UI nos métodos ConfigureServices e Configure da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //....
    services.AddHealthChecks()
            .AddDbContextCheck<TodoContext>()
            .AddSelfCheck("Self");

    services.AddHealthChecksUI();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...

    //Ativa o HealthChecks
    app.UseHealthChecks("/status", new HealthCheckOptions()
    {
        // WriteResponse is a delegate used to write the response.
        ResponseWriter = (httpContext, result) => {
            httpContext.Response.ContentType = "application/json";

            var json = new JObject(
                new JProperty("status", result.Status.ToString()),
                new JProperty("results", new JObject(result.Entries.Select(pair =>
                    new JProperty(pair.Key, new JObject(
                        new JProperty("status", pair.Value.Status.ToString()),
                        new JProperty("description", pair.Value.Description),
                        new JProperty("data", new JObject(pair.Value.Data.Select(
                            p => new JProperty(p.Key, p.Value))))))))));
            return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented));
        }
    });

    //Ativa o HealthChecks utilizado pelo HealthCheckUI
    app.UseHealthChecks("/status-api", new HealthCheckOptions()
    {
        Predicate = _ => true,
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    });

    app.UseHealthChecksUI(opt => {
        opt.UIPath = "/status-dashboard";
    });

    //...
}

Além de habilitar o Health Checks UI note que também é definido um novo endpoint para o health check, configurado para o delegate UIResponseWriter.WriteHealthCheckUIResponse. Este é um delegate criado pelo Health Checks UI, e configura um arquivo JSON que será lido pela biblioteca.

Para que ele seja lido, é necessário especificá-lo no arquivo appsettings.json:

{
  "HealthChecks-UI": {
    "HealthChecks": [
      {
        "Name": "Status-API",
        "Uri": "https://localhost:5001/status-api"
      }
    ]
  }
}

Com isso, ao executar a aplicação e acessar a URL /status-dashboard a interface será exibida:

Por hoje é isso 🙂

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

Lembrando que você pode ver o código da aplicação no Github.

.NET Core 3.0 – Criando tarefas de segundo plano com Worker Service

Quando se pensa em uma aplicação ASP.NET Core, provavelmente irá pensar em uma aplicação MVC, Web API ou Razor. Mas um detalhe pouco conhecido é que também é possível criar tarefas de segundo plano com o ASP.NET Core, através do conceito de generic hosts. Na versão 3.0 do .NET Core, este tipo de aplicação foi definida em um novo template chamado Worker Service, facilitando assim a criação das tarefas de segundo plano e dando mais destaque para este recurso.

O conceito de generic host foi introduzido na versão 2.1 do .NET Core e trata-se de uma aplicação ASP.NET Core que não processa requisições HTTP. O objetivo deste tipo de aplicação é remover o pipeline HTTP enquanto mantém os demais recursos de uma aplicação ASP.NET Core, como configuração, injeção de dependência e logging. Permitindo assim a criação de aplicações que precisam ser executadas em segundo plano, como serviços de mensagens.

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

Criando uma aplicação Worker Service

Para criar uma aplicação Worker Service é necessário ter instalado o .NET Core 3.0, que no momento da criação deste artigo está no preview 7. Caso esteja utilizando o Visual Studio, este tipo de aplicação está disponível a partir da versão 2019.

Como estou utilizando o Visual Studio Code, vou criar a aplicação pela linha de comando com o código abaixo:

dotnet new worker -n WorkerSample

No Visual Studio 2019 você pode encontrar o template desta aplicação dentro dos subtemplates do ASP.NET Core.

Na classe Program, podemos notar que é criado um servidor web:

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

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}

Que define um serviço, a classe Worker, que veremos a seguir.

Definindo a tarefa

A classe responsável por executar a tarefa em segundo plano é a classe Worker:

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Note que o construtor recebe uma instância de Logger. Podemos utilizar esta instância para notificar o usuário do estado do serviço. Já no método ExecuteAsync é onde a tarefa de segundo plano é executada.

Note que ela é executada enquanto o serviço não for cancelado.

Para este artigo iremos modificar apenas o método ExecuteAsync, adicionando informações sobre o estado do serviço:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("O serviço está iniciando.");

    stoppingToken.Register(() => _logger.LogInformation("Tarefa de segundo plano está parando."));

    while (!stoppingToken.IsCancellationRequested)
    {
        _logger.LogInformation("Executando tarefa: {time}", DateTimeOffset.Now);
        await Task.Delay(10000, stoppingToken);
    }

    _logger.LogInformation("O serviço está parando.");
}

Ao executar a aplicação temos o resultado abaixo:

No momento ela ainda não está sendo executada como serviço, vamos configurá-la para isso.

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

Configurando a tarefa como serviço para ser executada em segundo plano

Para executar a aplicação como serviço, é necessário adicionar o pacote “Microsoft.Extensions.Hosting.WindowsServices” na aplicação:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Ao adicioná-lo podemos chamar o método UseWindowsService():

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });

Este método verifica se a aplicação está executando como serviço e faz as configurações necessárias de acordo deste contexto. Ela também configura a aplicação para utilizar o ServiceBaseLifetime, que auxilia no controle do ciclo de vida da aplicação enquanto ela é executada como serviço. Isso sobrescreve o padrão ConsoleLifetime.

Publicando o serviço

Com o ServiceBaseLifetime configurado, podemos publicar a nossa aplicação:

dotnet publish -c Release

O comando acima irá publicar a aplicação de acordo com o ambiente de desenvolvimento. Você também pode publicar a aplicação para um sistema específico com a opção -r:

dotnet publish -c Release -r <RID>

“ significa Runtime Identifier (RID) e na documentação você pode ver as opções disponíveis.

Instalando o serviço no Windows

Com a aplicação publicada, ela pode ser instalada como serviço no Windows com o utilitário sc:

sc create workersample binPath=C:\Programs\Services\WorkerSample.exe

Instalando o serviço no Mac OS X

No caso do Mac OS X, os serviços são conhecimentos como daemon. Para definir o nosso serviço neste sistema como um daemon, é necessário possuir o .NET Core instalado na máquina e definir um script:

#!bin/bash
#Start WorkerSample if not running
if [ “$(ps -ef | grep -v grep | grep WorkerSample | wc -l)” -le 0 ]
then
 dotnet /usr/local/services/WorkerSample.dll
 echo "WorkerSampler Started"
else
 echo "WorkerSample Already Running"
fi

Dê para este script permissão de execução:

chmod +x ~/scripts/startup/startup.sh

Os daemons neste sistema operacional são definidos na pasta /Library/LaunchDaemons/, onde deve ser configurado em um arquivo XML com a extensão .plist (não faz parte do escopo deste artigo explicar as configurações deste arquivo, você pode conhecê-las na documentação do launchd).

No nosso caso, o arquivo terá o conteúdo abaixo:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>EnvironmentVariables</key>
    <dict>
      <key>PATH</key>
      <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:</string>
    </dict>
    <key>Label</key>
    <string>br.com.treinaweb.workersample</string>
    <key>Program</key>
    <string>/Users/admin/scripts/startup/startup.sh</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/>
    <key>LaunchOnlyOnce</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/startup.stdout</string>
    <key>StandardErrorPath</key>
    <string>/tmp/startup.stderr</string>
    <key>UserName</key>
    <string>admin</string>
    <key>GroupName</key>
    <string>admin</string>
    <key>InitGroups</key>
    <true/>
  </dict>
</plist>

Com isso, para iniciar o daemon basta carregar o arquivo .plist, que acabamos de definir:

sudo launchctl load -w /Library/LaunchDaemons/br.com.treinaweb.workersample.plist

Instalando o daemon no Linux

Assim como no Mac OS X, no Linux os serviços são conhecidos como daemon e nesta plataforma também é necessário possuir o .NET Core instalado na máquina. Fora isso, cada distribuição pode definir uma forma de criação de daemon, aqui vou explicar a criação utilizando o systemd.

No systemd o serviço é definido em um arquivo .service salvo em /etc/systemd/system e que no exemplo deste artigo conterá o conteúdo o abaixo:

[Unit]
Description=Dotnet Core Demo service

[Service]  
ExecStart=/bin/dotnet/dotnet WorkerSample.dll
WorkingDirectory=/usr/local/services
User=dotnetuser  
Group=dotnetuser  
Restart=on-failure  
SyslogIdentifier=dotnet-sample-service  
PrivateTmp=true  

[Install]  
WantedBy=multi-user.target  

Após isso, basta habilitar o daemon:

systemctl enable worker-sample.service 

Que ele poderá ser iniciado:

systemctl start dotnet-sample-service.service

Obs: Infelizmente no Mac OS X e Linux, a aplicação não sabe quando está sendo parada, algo que ocorre no Windows, quando ela é executada como serviço.

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

O Worker Service é bom?

O template Worker Service é uma boa adição para o .NET Core, pois isso pode tornar os generic hosts cada vez mais populares. Permitindo que as pessoas consigam utilizar os recursos do ASP.NET Core em uma aplicação console.

E com o tempo, espero que melhore o suporte da aplicação como daemon em ambientes Unix.

ASP.NET Core: Compreendendo AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages()

Neste artigo veremos as diferenças entres os métodos AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages() e quando utilizar cada um.

Tratando-se de uma reescrita do ASP.NET, a sua versão Core sempre foi pensada como um framework modular. Isso significa que as aplicações podem (e devem) adicionar apenas os recursos que utilizarão, o que irá otimizar a sua execução.

Infelizmente geralmente isso não é seguido, uma prova é a pouca adoção do método AddMvcCore().

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

AddMvcCore

Caso seja um iniciante no ASP.NET Core, talvez nunca tenha ouvido falar no método AddMvcCore(), mesmo na documentação oficial não é muito citado. Ele é um método de extensão de IServiceCollection, que carrega apenas os recursos básicos de uma aplicação ASP.NET:

  • Controllers: reconhece os controllers da aplicação;
  • Rotas: adiciona as rotas padrão;
  • CoreServiecs: adiciona os recursos básicos do ASP.NET.

Por adicionar os recursos básicos, qualquer outro recurso precisa ser adicionado “na mão”.

Por exemplo, crie uma aplicação web vazia e nela adicione um controller qualquer, com um método para a solicitação POST:

[Route("api/[controller]")]
public class HomeController : Controller
{
    [HttpPost]
        public ActionResult Post([FromBody] Usuario user)
    {
        if(ModelState.IsValid)
            return Ok(user);
        else
            return BadRequest(ModelState);
    }
}

E um model com ao menos um campo obrigatório:

public class Usuario
{
    public int Id { get; set; }
    public string Nome { get; set; }
    [Required]
    public string User { get; set; }
    [Required]
    public string Senha { get; set; }
}

Caso no método ConfigureServices seja adicionado apenas o método AddMvcCore():

public void ConfigureServices(IServiceCollection services) => services.AddMvcCore().AddNewtonsoftJson();

Obs.: Estou usando o .NET Core 3.0 Preview 4 e nesta versão as classes do Json.NET foram removidas da biblioteca padrão, assim, é necessário adicioná-las com o pacote Microsoft.AspNetCore.Mvc.NewtonsoftJson e o método AddNewtonsoftJson() utilizado acima.

O ASP.NET não irá validar os campos obrigatórios na solicitação POST:

Para que isso ocorra é necessário adicionar as Data Annotations:

public void ConfigureServices(IServiceCollection services) => services.AddMvcCore().AddNewtonsoftJson().AddDataAnnotations();

Com isso, a validação funcionará:

Claro que este nosso exemplo é simples, mas imagine ter que especificar autorização (a anotação [Authorize] ), CORS, etc. Por causa disso, geralmente utiliza-se o método AddMvc().

AddMvc

Assim como o AddMvcCore(), o AddMvc() é um método de extensão de IServiceCollection que carrega praticamente todos os recursos necessários de uma aplicação ASP.NET, como:

  • MvcCore: as dependências mínimas para executar uma aplicação ASP.NET Core MVC. Reconhece os controllers, realiza os bindings, adiciona das rotas padrão e os recursos básicos do ASP.NET;
  • ApiExplorer: Habilita as API Help pages;
  • Authorization: Habilita autorização;
  • FormatterMappings: Traduz a extensão de um arquivo para content-type;
  • Views: Habilita as Views;
  • RazorViewEngine: Habilita o Razor nas Views (páginas salvas com a extensão cshtml);
  • RazorPages: Habilita as Razor Pages (um tipo de aplicação do ASP.NET Core);
  • TagHelper: Adiciona as tags helpers do Razor;
  • Data Annotations: Habilita validação dos model por data annotations;
  • Json Formatters: Adiciona parsers de JSON;
  • CORS: Habilita o Cross-origin resource sharing (CORS).

Por adicionar vários recursos geralmente opta-se por definir apenas o método AddMvc(), em detrimento do AddMvcCore().

Ao fazer esta mudança na nossa aplicação:

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

Não é necessário especificar o Data Annotations, para que a validação seja realizada:

Mas caso esteja desenvolvendo uma aplicação Web API, o método AddMvc() irá carregar recursos que não serão utilizados. Nesta situação, o ideal seria optar pelo AddMvcCore(), mas se notar, até a versão 2.2 do ASP.NET a aplicação Web API criada pelo template padrão, também faz uso do método AddMvc().

Para alterar este comportamento, na versão 3.0 (que no momento da publicação deste artigo está em preview 4) foram adicionados os métodos AddControllers(), AddControllersWithViews() e AddRazorPages().

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

AddControllers

Podemos dizer que o método AddControllers() é uma evolução do AddMvcCore(), já que foi criado visando preencher a lacuna que este método não conseguiu. Para isso, como o seu nome sugere, ele carrega recursos relacionados aos controllers:

  • Controllers
  • Model Binding;
  • ApiExplorer;
  • Authorization;
  • CORS;
  • Data Annotations;
  • FormatterMappings.

Desta forma, se a aplicação não possuir uma interface, como a do nosso exemplo, que é uma Web API, pode-se optar por ele:

public void ConfigureServices(IServiceCollection services) => services.AddControllers().AddNewtonsoftJson();

Que todos os recursos relacionados a este tipo de aplicação serão carregados.

AddControllersWithViews

O método AddMvc() não foi descontinuado na versão 3.0, mas foi adicionando o método AddControllersWithViews() que carrega praticamente os mesmos recursos:

  • MvcCore;
  • ApiExplorer;
  • Authorization;
  • FormatterMappings;
  • Views;
  • RazorViewEngine;
  • TagHelper;
  • Data Annotations;
  • Json Formatters;
  • CORS.

O único que foi removido é o Razor Pages. Desta forma, uma aplicação ASP.NET MVC que não faça uso do Razor Pages deve optar por este método.

Para exemplificá-lo, é necessário adicionar uma view na aplicação:

@{
    ViewData["Title"] = "Home Page";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"]</title>
</head>
<body>
    <p>Aplicativo de exemplo!</p>
</body>
</html>

Definir uma action no controller:

public IActionResult Index()
{
    return View();
}

E definir as rotas no método Configure:

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

Com isso, a view será mostrada:

AddRazorPages

Razor Pages é um tipo de aplicação que permite adicionar códigos C# e HTML em um mesmo arquivo ou em arquivos separados, de uma forma muito similar ao antigo Web Forms. Por ser um aspecto de uma aplicação ASP.NET MVC, o AddRazorPages() carrega quase os mesmos recursos do método AddControllersWithViews():

  • RazorPages;
  • MvcCore;
  • Authorization;
  • Views;
  • RazorViewEngine;
  • TagHelper;
  • Data Annotations;
  • Json Formatters.

Os recursos que não são carregados por padrão são:

  • ApiExplorer;
  • FormatterMappings;
  • CORS.

Como o nome sugere este método deve ser utilizado em aplicações Razor Pages.

Mas caso a sua aplicação faça uso do Razor Pages e do ASP.NET MVC padrão, ele pode ser utilizado em conjunto com AddControllers() ou AddControllersWithViews():

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddControllersWithViews();
}

Que irá gerar o mesmo resultado do uso do AddMvc().

Conclusão

Por ser um framework modular, o ideal é que a aplicação ASP.NET carregue apenas os recursos que necessita. Comparando os métodos AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages(); e vendo quais recursos cada um carrega, fica claro a função de cada um e quando utilizá-los de acordo com cada tipo de aplicação.

Assim, procure sempre utilizar o método que for mais adequado ao tipo de aplicação que está desenvolvendo, mesmo se o template padrão do ASP.NET sugerir outro método.

Implementando o protocolo tus em uma aplicação ASP.NET Core

Upload de arquivo é uma tarefa recorrente de qualquer aplicação web. Não importando a linguagem, é simples a implementação deste recurso na aplicação. Mas quando se trata de grandes arquivos, nos deparamos com algumas situações pouco agradáveis.

Por exemplo, você precisa upar um arquivo grande, de 10GB para mais. Espera por horas pelo seu upload. Quando está quase acabando, a conexão cai, por algo no servidor, falta de internet, energia, etc. O upload será abortado e você terá que iniciá-lo do zero. Só quem já passou por isso, sabe o quanto é frustrante.

Caso não tenha uma boa conexão, mesmo com arquivos pequenos, o upload pode ser frustrante. Já que geralmente neste tipo de situação, o usuário envia apenas o início do arquivo várias vezes.

Para tentar resolver este tipo de problema foi criado o protocolo tus.

O que é o protocolo tus?

tus é um protocolo aberto para “recuperação” de upload de arquivo em HTTP. Isso significa que o usuário não precisa reiniciar o upload sempre que ocorre um erro no processo ou mesmo enviar todo o arquivo em apenas uma conexão. Permitindo que o usuário pause o processo ou mesmo o envie em uma conexão instável.

Eles possuem uma comunidade bem ativa no Github e grandes empresas que o implementam, como o Cloudflare e o Vimeo.

Funcionamento do protocolo

Na documentação do protocolo é possível ver como ele funciona em detalhes, mas o processo é simples. O servidor recebe solicitações POST, sem conteúdo, que contenha no seu header, os atributos abaixo:

  • Upload-Length: O tamanho, em bytes, do arquivo que será enviado;
  • Upload-Metadata: Atributo opcional, que pode ser utilizado para enviar informações do arquivo para o servidor, como nome, extensão, etc. Essas informações devem ser organizadas em pares de chave-valor, separados por vírgula. A chave e valor devem ser separados por espaço. Assim, os pares de chave-valor não podem conter espaço ou vírgula;
  • Tus-Resumable: Versão do protocolo.

Ele irá responder, com o código 201 (Created) e com os atributos abaixo no header:

  • Location: URL que deve ser utilizada para o upload do arquivo;
  • Tus-Resumable: Versão do protocolo.

Com isso, o usuário poderá utilizar a URL retornada na solicitação POST para efetuar o upload do arquivo. Este upload deve ser feito em solicitações PATCH e que contenha os atributos abaixo:

  • Upload-Offset: Indica em qual ponto do arquivo (baseado em um array de bytes) o conteúdo enviado deve ser adicionado;
  • Content-Length: Tamanho do conteúdo enviado;
  • Content-Type: Tipo do conteúdo, que sempre deve ser application/offset+octet-stream;
  • Tus-Resumable: Versão do protocolo.

Essas solicitações são respondidas com o código 204 (No Content) e com os atributos abaixo no header:

  • Upload-Offset: Valor atual do upload-offset;
  • Tus-Resumable: Versão do protocolo.

O servidor também pode aceitar solicitações DELETE, que permite excluir um upload não finalizado e OPTIONS, que retorna informações do uso do protocolo, como versão (Tus-Version), recursos implementados (Tus-Extension) e tamanho máximo de arquivo aceito (Tus-Max-Size).

Implementando em uma aplicação ASP.NET Core

Pelo funcionamento, notamos que a sua implementação não é complexa, mas requer um pouco de trabalho. Felizmente há bibliotecas que facilitam a implementação deste protocolo em uma aplicação ASP.NET Core.

Para mostrá-lo na prática, vamos criar uma aplicação de exemplo:

dotnet new web -n ExemploTus

Na página do protocolo são listadas algumas implementações dele, tanto para o lado do client quanto para o server. No caso do .NET, a implementação é recomentada para o servidor é a do Stefan Matsson, o tusdotnet. Assim, a adicione no projeto:

dotnet add package tusdotnet

Para configurá-lo, na classe Startup, no método Configure adicione o código abaixo:

app.UseTus(context => new DefaultTusConfiguration
{
    //Local onde os arquivos temporários serão salvos
    Store = new TusDiskStore(tempPath),
    // URL onde os uploads devem ser realizados.
    UrlPath = "/upload",
    Events = new Events
    {
        //O que fazer quando o upload for finalizado
        OnFileCompleteAsync = async ctx =>
        {
            var file = await ((ITusReadableStore)ctx.Store).GetFileAsync(ctx.FileId, ctx.CancellationToken);
            await ProcessFile.SaveFileAsync(file, env);
        }
    }
});

A biblioteca possui vários eventos, na documentação dela você pode conhecer quais são gerados.

No nosso caso, estamos apenas definindo a URL que será utilizada para o upload, o local onde os arquivos temporários serão salvos e estamos salvando em outro ponto do disco o arquivo quando seu upload for finalizado.

Não se esqueça de alterar o tamanho limite de arquivos aceitos pela aplicação. Isso pode ser feito no método CreateWebHostBuilder da classe Program:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureKestrel(options =>
        {
            options.Limits.MaxRequestBodySize = null;
            options.Limits.MaxRequestBufferSize = null;
        });

Acima está sendo definido um limite “infinito”, mas você pode informar qualquer valor em bytes.

Para testar isso, vamos utilizar a biblioteca JS disponibilizada pelo tus, que pode ser baixada aqui.

Com isso, poderemos definir um arquivo HTML, onde o upload será testado:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Upload de arquivo</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body>
    <div class="jumbotron jumbotron-fluid">
        <div class="container">
            <h1 class="display-4">Upload de arquivo</h1>
            <p class="lead">
                Para testar o protocolo, selecione um arquivo e clique em "Upload". Aguarde um pouco cancele, feche o navegador ou atualizado a página e inicie novamente 🙂
            </p>
            <p>
                <a href="#" onclick="resetLocalCache(event)">Clique aqui para limpar o cache do navegador!</a>
            </p>

            <form>
                <div class="custom-file">
                    <input type="file" class="custom-file-input" id="fileUpload" onchange="alterarNome()">
                    <label class="custom-file-label" for="fileUpload" id="fileUploadLabel">Selecione o arquivo</label>
                </div>
                <div class="form-group mt-2 text-right">
                    <input type="button" id="uploadButton" value="Upload" onclick="uploadFile()" class="btn btn-primary mb-2" />
                    <input type="button" id="cancelUploadButton" value="Cancelar" onclick="cancelUpload()" class="btn btn-primary mb-2" disabled />
                </div>
            </form>
            <progress value="0" max="100" id="uploadProgress" class="w-100" style="display:none" ></progress>
            <span id="info"></span>
        </div>
    </div>
    <script src="tus.js"></script>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    <script>
        var uploadProgress = document.getElementById('uploadProgress');
        var info = document.getElementById('info');
        var cancelUploadButton = document.getElementById('cancelUploadButton');
        var uploadButton = document.getElementById('uploadButton');
        var fileUpload = document.getElementById('fileUpload');
        var fileUploadLabel = document.getElementById('fileUploadLabel');
        var upload;

        function uploadFile() {
            var file = fileUpload.files[0];

            uploadProgress.value = 0;
            uploadProgress.removeAttribute('data');
            uploadProgress.style.display = 'block';

            disableUpload();

            info.innerHTML = '';

            upload = new tus.Upload(file,
                {
                    endpoint: 'upload/',
                    onError: onTusError,
                    onProgress: onTusProgress,
                    onSuccess: onTusSuccess,
                    metadata: {
                        filename: file.name,
                        contentType: file.type || 'application/octet-stream'
                    }
                });

            setProgressTest('Upload iniciado...');
            upload.start();
        }

        function cancelUpload() {
            upload && upload.abort();
            setProgressTest('Upload abortado');
            uploadProgress.value = 0;
            enableUpload();
        }

        function resetLocalCache(e) {
            e.preventDefault();
            localStorage.clear();
            alert('Cache limpo');
        }

        function onTusError(error) {
            alert(error);
            enableUpload();
        }

        function onTusProgress(bytesUploaded, bytesTotal) {
            var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);

            uploadProgress.value = percentage;
            setProgressTest(bytesUploaded + '/' + bytesTotal + ' bytes uploado');
        }

        function onTusSuccess() {
            setProgressTest('Upload finalizado!');
            enableUpload();
        }

        function setProgressTest(text) {
            info.innerHTML = text;
        }

        function enableUpload() {
            uploadButton.removeAttribute('disabled');
            cancelUploadButton.setAttribute('disabled', 'disabled');
        }

        function disableUpload() {
            uploadButton.setAttribute('disabled', 'disabled');
            cancelUploadButton.removeAttribute('disabled');
        }

        function alterarNome(){
            var fileName = fileUpload.files[0].name;
            fileUploadLabel.innerHTML = fileName;
        }
    </script>

</body>
</html>

Ao executar a aplicação, poderemos testar o upload de um arquivo grande e ver o resultado:

Você pode ver o código desta aplicação de exemplo no meu Github, aqui.

Conclusão

Caso a sua aplicação necessite efetuar o upload de arquivos grandes, devido ao seu poder, adicione o suporte ao protocolo tus. Isso irá facilitar muito o envio de arquivos para os usuários.

E como vimos, é muito simples implementar este suporte em uma aplicação ASP.NET Core. Assim, não há desculpa para não utilizar este protocolo.

Criando um provider customizado para o Microsoft.Extensions.Logging

Caso já tenha desenvolvido uma aplicação ASP.NET Core, deve ter deparado com o seu sistema de log. Adicionado desde a primeira versão desta biblioteca, o ILoggingBuilder da Microsoft.Extensions.Logging é o responsável por registrar nos Services provedores de log para este tipo de aplicação.

Internamente, o provedor recebe do LoggerFactory todas as informações geradas durante a execução da aplicação, tanto em tempo de desenvolvimento quanto em produção.

Mesmo com uma série de provedores (nativos e de terceiros), em alguns casos é necessário criar um provedor customizado e neste artigo veremos como isso pode ser feito.

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

Anteriormente em um projeto ASP.NET Core…

Antes de vermos a criação do provedor, um resumo da biblioteca Microsoft.Extensions.Logging.

Esta biblioteca foi criada como uma API de log, que os desenvolvedores podem utilizar para capturar os logs internos do ASP.NET Core, bem como gerar logs customizados. Esses logs são enviados para provedores, que ficam responsáveis de registrar as mensagens.

No momento, há os seguintes provedores nativos:

E também temos os seguintes provedores de bibliotecas de terceiro:

Caso nenhuma dessas bibliotecas atendam as suas necessidades, chega a hora de criar um provedor customizado, é o que faremos a seguir.

Primeiro Ato – Preparando o ambiente

Neste exemplo, iremos criar um provedor que registre os logs dos dados em uma tabela do SQLite, então antes de criá-lo precisamos preparar o ambiente.

Neste artigo estou utilizando uma aplicação ASP.NET Core 2.2. Então, após a criação dela, adicione a biblioteca do SQLite:

dotnet add package System.Data.SQLite

E do Dapper (caso queira, você pode optar por outra biblioteca ORM):

dotnet add package Dapper

Já a configuração desta conexão, está seguindo o mesmo padrão do meu artigo do Dapper, que você pode ver aqui. A diferença é que para o provedor será definida a entidade/model abaixo:

public class EventLog
{
    public int Id { get; set; }
    public string Category { get; set; }
    public int EventId { get; set; }
    public string LogLevel { get; set; }
    public string Message { get; set; }
    public DateTime CreatedTime { get; set; }
}

No próximo tópico veremos a implementação do repositório desta entidade.

Segundo Ato – Criando o provedor

Para criar um provedor para o LoggerFactory é necessário criar ao menos duas classes, que implementem, respectivamente, as interfaces ILogger e ILoggerProvider. Também é recomendado a criação de um método de extensão, seguindo o padrão Add{nome provedor}, que facilitará a sua adição.

E como implementaremos um provedor para o SQLite, implementaremos um repositório abstrato. Este repositório será uma propriedade da nossa classe “ILogger. Assim, de início defina este repositório:

public abstract class LoggerRepository : AbstractRepository<EventLog>
{
    public LoggerRepository(IConfiguration configuration) : base(configuration){}
}

Esta classe herda a classe AbstractRepository que demonstrei no artigo do Dapper.

Aproveitando, já defina a EventLogRepository:

public class EventLogRepository : LoggerRepository
{
    public EventLogRepository(IConfiguration configuration) : base(configuration){}

    public override void Add(EventLog item)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "INSERT INTO EventLog (Category, EventId, LogLevel, Message, CreatedTime)"
                            + " VALUES(@Category, @EventId, @LogLevel, @Message, @CreatedTime)";
            dbConnection.Open();
            dbConnection.Execute(sQuery, item);
        }
    }

    public override IEnumerable<EventLog> FindAll()
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Open();
            return dbConnection.Query<EventLog>("SELECT * FROM EventLog");
        }
    }

    public override EventLog FindByID(int id)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "SELECT * FROM EventLog" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            return dbConnection.Query<EventLog>(sQuery, new { Id = id }).FirstOrDefault();
        }
    }

    public override void Remove(int id)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "DELETE FROM EventLog" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            dbConnection.Execute(sQuery, new { Id = id });
        }
    }

    public override void Update(EventLog item)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "UPDATE EventLog SET Category = @Category,"
                        + " EventId = @EventId, LogLevel= @LogLevel," 
                        + " Message = @Message, CreatedTime= @CreatedTime" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            dbConnection.Query(sQuery, item);
        }
    }
}

Agora podemos definir a nossa classe ILogger, que será chamada SqliteLogger:

public class SqliteLogger<T> : ILogger where T: LoggerRepository
{
    private Func<string, LogLevel, bool> _filter;
    private T _repository;
    private string _categoryName;
    private readonly int maxLength = 1024;
    private IExternalScopeProvider ScopeProvider { get; set; }

    public SqliteLogger(Func<string, LogLevel, bool> filter, T repository, string categoryName)
    {
        _filter = filter;
        _repository = repository;
        _categoryName = categoryName;
    }

    public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;

    public bool IsEnabled(LogLevel logLevel) => (_filter == null || _filter(_categoryName, logLevel));

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        var logBuilder = new StringBuilder();

        if (!IsEnabled(logLevel)) 
        { 
            return; 
        } 
        if (formatter == null) 
        { 
            throw new ArgumentNullException(nameof(formatter)); 
        } 
        var message = formatter(state, exception);
        if (!string.IsNullOrEmpty(message)) 
        { 
            logBuilder.Append(message);
            logBuilder.Append(Environment.NewLine);
        }

        GetScope(logBuilder);

        if (exception != null)
            logBuilder.Append(exception.ToString());

        if (logBuilder.Capacity > maxLength)
            logBuilder.Capacity = maxLength;

        var eventLog = new EventLog 
        { 
            Message = message, 
            EventId = eventId.Id,
            Category = _categoryName,
            LogLevel = logLevel.ToString(), 
            CreatedTime = DateTime.UtcNow 
        };

        _repository.Add(eventLog);
    }

    private void GetScope(StringBuilder stringBuilder)
    {
        var scopeProvider = ScopeProvider;
        if (scopeProvider != null)
        {
            var initialLength = stringBuilder.Length;

            scopeProvider.ForEachScope((scope, state) =>
            {
                var (builder, length) = state;
                var first = length == builder.Length;
                builder.Append(first ? "=> " : " => ").Append(scope);
            }, (stringBuilder, initialLength));

            stringBuilder.AppendLine();
        }
    }
}

Note que esta é uma classe parametrizada, que define que o tipo de dados deve herdar a classe LoggerRepository:

public class SqliteLogger<T> : ILogger where T: LoggerRepository

No construtor da classe, é recebido um predicado, o repositório e a categoria:

public SqliteLogger(Func<string, LogLevel, bool> filter, T repository, string categoryName)
{
    _filter = filter;
    _repository = repository;
    _categoryName = categoryName;
}

O predicado é um filtro que definirá qual nível de log deve ser registrado. Já a categoria é o valor definido na criação do logger.

No método BeginScope, adicionamos os escopos definidos em um provider de escopo:

public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;

Esses escopos são iterados e posteriormente adicionados na mensagem que será salva no banco.

No método IsEnabled:

public bool IsEnabled(LogLevel logLevel) => (_filter == null || _filter(_categoryName, logLevel));

Verifica, de acordo com o filtro, se o log deve ser registrado ou não.

Já no método Log é onde montamos a nossa mensagem e a salvamos no banco de dados (através do repositório).

Com o SqliteLogger definido, podemos criar o provedor em si:

public class SqliteLoggerProvider<T>: ILoggerProvider where T : LoggerRepository
{
    private readonly Func<string, LogLevel, bool> _filter;
    private readonly T _repository;

    public SqliteLoggerProvider(Func<string, LogLevel, bool> filter, T repository)
    {
        this._filter = filter;
        this._repository = repository;
    }

    public ILogger CreateLogger(string categoryName) => new SqliteLogger<T>(_filter, _repository, categoryName);

    public void Dispose() {}
}

Esta classe também recebe no seu construtor um predicado de filtro e o repositório. Já no método CreateLogger é retornado uma instância do nosso logger, passando a categoria informada:

public ILogger CreateLogger(string categoryName) => new SqliteLogger<T>(_filter, _repository, categoryName);

Agora é necessário definir o método de extensão:

public static class SqliteLoggerExtensions
{
    public static ILoggingBuilder AddSqliteProvider<T>(this ILoggingBuilder builder, T repository) where T: LoggerRepository
    {
        builder.Services.AddSingleton<ILoggerProvider, SqliteLoggerProvider<T>>(p => new SqliteLoggerProvider<T>((_, logLevel) => logLevel >= LogLevel.Debug, repository));

        return builder;
    }
}

Neste método de extensão é definido que o level mínimo do log é o Debug, que é o mais baixo. Isso significa que todas as mensagens de log serão registrados.

Terceiro Ato – A hora da verdade

Com o provedor definido, podemos utilizá-lo. Na atual versão do ASP.NET Core (2.2), isso deve ser feito no método CreateWebHostBuilder da classe Program, conforme abaixo:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddSqliteProvider(new EventLogRepository(hostingContext.Configuration));
        });

Ao executar a aplicação, todos os logs do ASP.NET Core serão salvos no nosso banco de dados:

Qualquer log definido dentro da aplicação, como o abaixo:

public class HomeController : Controller
{
    private readonly ILogger _logger;

    public HomeController(ILogger<HomeController> logger) => _logger = logger;

    public IActionResult Index()
    {
        _logger.LogInformation("Chamando a página inicial do site");

        return View();
    }

    //...
}

Também será salvo no banco.

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

Epílogo

Mesmo com várias opções de provedores para o Microsoft.Extensions.Logging, às vezes há a necessidade de criar um provedor customizado. Felizmente, como vimos, a criação deste tipo de provedor não é complexa. Então, quando não achar uma solução que o atende, opte pela implementação customizada.

Você pode ver o código da aplicação utilizada neste artigo no meu Github, aqui.

Criando uma API RESTful com o NancyFx e .NET Core

Quando pensamos em API RESTful no C#, a primeira opção que vem a mente é o ASP.NET Web API. Ele é um ótimo framework, utilizado em vários sistemas, com uma abundante documentação e material de apoio. Mas ele não é a nossa única opção para a criação de API Rest.

Em artigos passados, conhecemos o ServiceStack, mas outra opção que está crescendo é o NancyFX.

Inspirado no framework Sinatra do Ruby, o Nancy é um framework de criação de serviços HTTP, leve e direto ao ponto. Tem por objetivo não atrapalhar o desenvolvedor. Ele obtém isso através de uma série de configurações padrões e convenções. Assim, com o Nancy é possível criar um site em minutos.

Claro que todas as suas convenções e configurações podem ser alteradas, caso este seja o desejo do desenvolvedor.

Para este artigo, mostrarei como é possível criar uma API simples, rapidamente utilizando o Nancy.

Criando a aplicação

Neste artigo estou utilizando o .NET Core, desta forma, mostrarei como criar a aplicação através do terminal. Inicialmente é necessário criar uma aplicação web vazia:

dotnet new web -n NancyAPI

No projeto criado, adicione a referência do Nancy:

dotnet add package Nancy --version 2.0.0-clinteastwood

Agora é necessário dizer ao ASP.NET que iremos utilizar o Nancy, para isso, faremos uso do OWIN.

O OWIN significa “Open Web Interface for .Net. Ele é um conjunto de padrões voltados para o .NET, que visa facilitar e encorajar a implementação de projetos que tentam desacoplar a aplicação do servidor. Na prática ele define um middleware ao pipeline de execução de uma aplicação ASP.NET.

Por se tratar de um middleware, deve ser configurado no método Configure da classe Startup:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseOwin(o => o.UseNancy());
}

Com isso, o Nancy já está ativo na nossa aplicação.

Módulos

No Nancy os endpoints devem ser definidos em módulos. Um módulo nada mais é que uma classe que herde a classe NancyModule. Esta classe pode ser definida em qualquer ponto do projeto, pois o Nancy fará uma busca pelos módulos quando um endpoint for invocado. O único ponto importante é que esta classe seja definida como pública (public) para que possa ser localizada.

Por exemplo:

using Nancy;

namespace NancyAPI.Module
{
    public class HomeModule : NancyModule
    {
        public HomeModule()
        {
            Get("/", _ => "Hello World from Nancy!");
        }
    }
}

Note que a rota está sendo definida como o primeiro parâmetro do método Get. Como é possível supor, esta rota será invocada quando for solicitada através de uma requisição GET.

O Nancy define métodos para os principais verbos do HTTP:

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

No segundo parâmetro do método deve ser passado a referência de uma função ( uma Function), que no exemplo acima, se utiliza lambda para definir a função. Este é o padrão recomendado pelo Nancy.

Ao executar a aplicação e acessar o endpoint definido, teremos o resultado:

Simples, não é?

Definindo a API RESTful

Para exemplificar uma API REST, vamos definir um modelo:

namespace NancyAPI.Models
{
    public class Pessoa
    {
        public int Id { get; set; }
        public string Nome { get; set; }
    }
}

E um repositório:

using System.Collections.Generic;
using NancyAPI.Models;
using System.Linq;

namespace NancyAPI.Repositories
{
    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);
        }
    }
}

Agora só falta o nosso endpoint:

using Nancy;
using Nancy.ModelBinding;
using NancyAPI.Models;
using NancyAPI.Repositories;

namespace NancyAPI.Module
{
    public class PessoaModule: NancyModule
    {
        public readonly PessoaRepository repository;
        public PessoaModule()
        {
            repository = new PessoaRepository();

            Get("/pessoa/", _ => repository.GetAll());
            Get("/pessoa/{id}", args => repository.Get(args.id));
            Post("/pessoa/", args => {
                var pessoa = this.Bind<Pessoa>();

                repository.Add(pessoa);

                return pessoa;
            });
            Put("/pessoa/{id}", args => {
                var pessoa = this.Bind<Pessoa>();

                pessoa.Id = args.id;

                repository.Edit(pessoa);

                return pessoa;
            });
        }
    }
}

Neste endpoint é possível notar que os argumentos da URL são obtidos através do parâmetro do lambda:

Get("/pessoa/{id}", args => repository.Get(args.id));

Este parâmetro é um objeto dinâmico. Assim, o Nancy tentará obter o valor do argumento com base na propriedade informada.

Já os dados passados na solicitação, podem ser obtidos pelo Bind:

var pessoa = this.Bind<Pessoa>();

O Nancy irá converter automaticamente os dados obtidos para o tipo de objeto indicado.

Para testar os endpoints, podemos utilizar o Postman:

POST

GET

PUT

GET ID

Conclusão

Graças a sua estrutura simples, porém sofisticada, o Nancy tem atraído cada vez mais a atenção dos desenvolvedores. Se tornando uma ótima alternativa ao ASP.NET Web API.

Caso necessite criar uma API REST, não esqueça de dar uma boa olhada nele.

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

Você pode ver o projeto apresentado neste artigo no meu GitHub.

Documentando uma ASP.NET Core Web API com o Swagger

Com cada vez mais aplicações multiplataformas, é comum definir uma API REST que será consumida pelas várias versões da aplicação. Mas com vários times tendo acesso a API, não basta mais apenas desenvolvê-la e esperar que todos consigam utilizá-la institivamente.

Na época do web service, existia o WSDL que funcionava como uma documentação, já que facilitava a criação dos clientes que iria consumí-lo. Com as APIs REST não temos esta facilidade, assim, torna-se imprescindível que ela seja bem documentada.

Mas como realizar esta documentação? Existem algumas ferramentas que podem nos auxiliar neste processo, como: API Blueprint, RAML, Swagger, entre outras.

Como o nome deste artigo sugere, aqui abordaremos o Swagger.

Swagger

O Swagger é uma aplicação open source que auxilia os desenvolvedores a definir, criar, documentar e consumir APIs REST. Sendo uma das ferramentas mais utilizadas para esta função, a empresa por trás dela (a SmartBear Software), decidiu criar o Open API Iniciative e renomearam as especificações do Swagger para OpenAPI Specification.

Ela visa padronizar as APIs REST, desta forma descreve os recursos que uma API deve possuir, como endpoints, dados recebidos, dados retornados, códigos HTTP, métodos de autenticação, entre outros.

Para facilitar o processo de especificação/documentação de uma API, o Swagger fornece algumas ferramentas, como:

  • Swagger Editor: Editor que permite especificar uma API. Há uma versão online grátis e aplicativos para desktop;
  • Swagger UI: Interface interativa para a documentação da API;
  • Swagger Codegen: Gera templates de código (mais de 20 linguagens estão disponíveis) de acordo com uma especificação de API.

Neste artigo vamos focar na documentação de uma API existente, criada em ASP.NET Core Web API.

Especificação do Swagger

O ponto mais importante do Swagger é a sua especificação, que era chamada de Swagger specification e agora é OpenAPI Specification. Esta especificação trata-se de um documento, JSON ou YAML, que define a estrutura da API. Indicando os endpoints, formatos de entrada, de saída, exemplos de requisições, forma de autenticação, etc.

Abaixo você pode ver um exemplo desta especificação definida em JSON:

{
   "swagger": "2.0",
   "info": {
       "version": "v1",
       "title": "API V1"
   },
   "basePath": "/",
   "paths": {
       "/api/Example": {
           "get": {
               "tags": [
                   "Example"
               ],
               "operationId": "ApiExampleGet",
               "consumes": [],
               "produces": [
                   "text/plain",
                   "application/json",
                   "text/json"
               ],
               "responses": {
                   "200": {
                       "description": "Success",
                       "schema": {
                           "type": "array",
                           "items": {
                               "$ref": "#/definitions/Item"
                           }
                       }
                   }
                }
           },
           "post": {
               ...
           }
       },
       "/api/Example/{id}": {
           "get": {
               ...
           },
           "put": {
               ...
           },
           "delete": {
               ...
   },
   "definitions": {
       "Item": {
           "type": "object",
            "properties": {
                "id": {
                    "format": "int64",
                    "type": "integer"
                },
                "name": {
                    "type": "string"
                },
                "isValid": {
                    "default": false,
                    "type": "boolean"
                }
            }
       }
   },
   "securityDefinitions": {}
}

É possível definir esta especificação na mão, olhando as opções na documentação, através do Swagger Editor ou utilizando outra ferramenta para gerá-la.

No ASP.NET Core podemos utilizar duas bibliotecas para gerar este documento:

Por ser mais completa, faremos uso da Swashbuckle.

Adicionando o Swashbuckle na aplicação

Para este exemplo, estou utilizando uma aplicação ASP.NET Core Web API, baseada no exemplo fornecido pela documentação da Microsoft, que pode ser vista aqui. Você pode ver a aplicação que criei no meu Github.

Com a aplicação Web API criada, inicialmente é necessário adicionar o pacote do Swashbuckle na aplicação. Ele possui três componentes:

Mas é necessário instalar apenas o pacote Swashbuckle.AspNetCore. Por se tratar de uma aplicação .NET Core, este pacote pode ser adicionado com o comando abaixo:

dotnet add package Swashbuckle.AspNetCore

Após isso, para que o arquivo de especificação da API do Swagger seja criado, é necessário adicionar o gerador dele nos serviços da aplicação, no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //... código omitido

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });
    });
}

Para a classe Info, defina o using do namespace abaixo:

using Swashbuckle.AspNetCore.Swagger;

Agora, no método Configure é necessário ativar o Swagger e indicar o local onde o seu arquivo de especificação será criado:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //Ativa o Swagger
    app.UseSwagger();

    // Ativa o Swagger UI
    app.UseSwaggerUI(opt =>
    {
        opt.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI V1");
    });

    //... código omitido
}

Com isso, através de reflection a biblioteca consegue especificar os endpoints da aplicação.

Caso seja executada, podemos acessar o Swagger UI da aplicação no caminho /swagger:

Caso queira que o Swagger UI seja acessado a partir da raiz da aplicação, na ativação dela, no método Configure, atribua vazio para a propriedade RoutePrefix:

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI V1");
    c.RoutePrefix = string.Empty;
});

Customizando o Swagger

Como o Swashbuckle trabalha com reflection, podemos fazê-lo gerar mais informações das nossas APIs com base em configurações definidas nela.

Comentários de documentação

Sabemos que no C# podemos adicionar comentários de documentação nos nossos códigos, como:

/// <summary>
/// Lista os itens da To-do list.
/// </summary>
/// <returns>Os itens da To-do list</returns>
/// <response code="200">Returna os itens da To-do list cadastrados</response>
[HttpGet]
public ActionResult<List<Item>> Get()
{
    return _repository.GetAll();
}

É possível fazer o Swashbuckle ler esses comentários e assim tornar a especificação da API mais detalhada.

Para isso, na definição de geração do arquivo de especificação, no método ConfigureServices deve ser adicionado a informação abaixo:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);
});

Note que estamos indicando que ele deve ler os comentários do código a partir de um arquivo XML. Para que eles sejam gerados neste arquivo indicado, no arquivo de configuração do projeto (o .csproj), adicione o trecho abaixo:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

O <NoWarn> é definido para que a IDE não exiba um alerta sobre os códigos que não possuírem comentários de documentação.

Agora ao acessar o Swagger API, o que foi definido em <summary> será mostrado:

Também podemos o <remarks> para indicar um exemplo de requisição:

// POST api/todo
/// <summary>
/// Cria um item na To-do list.
/// </summary>
/// <remarks>
/// Exemplo:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item1",
///        "iscomplete": true
///     }
///
/// </remarks>
/// <param name="value"></param>
/// <returns>Um novo item criado</returns>
/// <response code="201">Retorna o novo item criado</response>
/// <response code="400">Se o item não for criado</response>        
[HttpPost]
public ActionResult<Item> Post([FromBody] Item value)
{
    Console.WriteLine(value?.Name);

    var item = _repository.Save(value);
    if(item != null)
        return item;

    return BadRequest();
}

O resultado será:

Com as anotações ProducesResponseType e Produces podemos indicar os códigos HTTP e o tipo de dado que os endpoints irão retornar:

[Authorize]
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
    //..código omitido

    // GET api/todo
    /// <summary>
    /// Lista os itens da To-do list.
    /// </summary>
    /// <returns>Os itens da To-do list</returns>
    /// <response code="200">Returna os itens da To-do list cadastrados</response>
    [HttpGet]
    [ProducesResponseType(200)]
    public ActionResult<List<Item>> Get()
    {
        return _repository.GetAll();
    }

    //... código omitido
}

Especificando a autenticação

Uma das principais vantagens de adicionar o Swagger na aplicação, é poder testar os endpoints pela Swagger UI. Quando a aplicação define autenticação, é necessário indicar isso para o Swashbuckle, para que ele também gere uma especificação desde detalhe.

Com isso, será possível testar os endpoints pela Swagger UI utilizando a autenticação definida.

Para fazer isso, devemos adicionar a informação abaixo, na definição de geração do arquivo de especificação, no método ConfigureServices:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);

    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };

    opt.AddSecurityDefinition(
        "Bearer",
        new ApiKeyScheme
        {
            In = "header",
            Description = "Copie 'Bearer ' + token'",
            Name = "Authorization",
            Type = "apiKey"
        });

    opt.AddSecurityRequirement(security);
});

Acima estamos definindo que a aplicação faz uso de autenticação do tipo JWT Token (“Bearer Token”). Na documentação do Swagger, você pode ver os outros tipos de autenticação suportados.

Ao acessar o Swagger UI, ele irá mostrar o botão “Autorize”:

Exibe um botão com o texto "Autorize"

Ao clicar nele, é possível informar o tipo de autenticação especificado:

Com isso, mesmo que necessite de autenticação, será possível realizar as requisições dos endpoints da aplicação pelo Swagger UI.

Descrição

Por fim, é possível descrever um pouco da aplicação através das propriedades da classe Info da especificação do Swagger:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info
    {
        Version = "v1",
        Title = "Todo API",
        Description = "Um exemplo de aplicação ASP.NET Core Web API",
        TermsOfService = "Não aplicável",
        Contact = new Contact
        {
            Name = "Wladimilson",
            Email = "contato@treinaweb.com.br",
            Url = "https://treinaweb.com.br"
        },
        License = new License
        {
            Name = "CC BY",
            Url = "https://creativecommons.org/licenses/by/4.0"
        }
    });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);

    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };

    opt.AddSecurityDefinition(
        "Bearer",
        new ApiKeyScheme
        {
            In = "header",
            Description = "Copie 'Bearer ' + token'",
            Name = "Authorization",
            Type = "apiKey"
        });

    opt.AddSecurityRequirement(security);
});

O resultado será:

Swagger UI mostrando a descrição da API

Conclusão

O Swagger é uma ótima forma de documentar APIs REST e com a biblioteca Swashbuckle, podemos gerar o arquivo de especificação do Swagger com facilidade. Assim, não há mais desculpa para se definir documentações interativas e detalhadas para as suas APIs REST.

Compreendendo os middlewares no ASP.NET Core

Desde a sua primeira versão, o ASP.NET faz uso dos middlewares. Eles foram implementados como uma forma de modularizar uma aplicação ASP.NET facilmente.

Em termos práticos, middleware seria um trecho de código que pode ser executado no fluxo de execução da aplicação. No ASP.NET os middleware são organizados em um pipeline e são executados conforme uma solicitação é recebida e uma resposta enviada. A imagem abaixo ilustra este pipeline:

A imagem apresenta uma solicitação sendo processada por três middlewares, sendo que cada um chamado o middleware seguinte. No terceiro middleware, uma resposta é retornada e esta passa pelos middleware chamados anteriormente.

Cada middleware pode executar uma ação no recebimento da solicitação, chamar o próximo middleware, utilizando o método next(), e executar outra ação durante o retorno da resposta. Só não é possível modificar a resposta no seu retorno.

Dependendo da funcionalidade, o middleware pode decidir não chamar o próximo no pipeline, não invocando o método next(). Por exemple, o middleware de arquivos estáticos pode retornar uma solicitação para um arquivo estático e interromper o fluxo restante.

É possível definir middleware de diversas funcionalidades, por exemplo, no trecho de código abaixo, temos oito middlewares:

  1. Exception/error handling
  2. HTTP Strict Transport Security Protocol
  3. HTTPS redirection
  4. Static file server
  5. Cookie policy enforcement
  6. Authentication
  7. Session
  8. MVC
public void Configure(IApplicationBuilder app)
{
    if (env.IsDevelopment())
    {
        // Quando executado em desenvolvimento:
        //   Utiliza Developer Exception Page para reportar erros.
        //   Utiliza Database Error Page para reportar erros do banco.
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        // Quando não estiver em produção:
        //   Habilita o middleware Exception Handler Middleware para pegar os erros.
        //   Utiliza o middleware que habilita o 
        //       HTTP Strict Transport Security Protocol (HSTS)
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    // Utiliza o middleware HTTPS Redirection que redireciona solicitações HTTP para HTTPS.
    app.UseHttpsRedirection();

    // Retorna arquivos estáticos e interrompe o pipeline.
    app.UseStaticFiles();

    // Utiliza o middleware Cookie Policy, que está em conformidade com 
    // as regras do GDPR (General Data Protection Regulation).
    app.UseCookiePolicy();

    // Autentica antes de utilizar os recursos.
    app.UseAuthentication();

    // Se o aplicativo utiliza sessão, chama o middleware Session depois do middleware 
    // Cookie Policy e antes do middleware MVC.
    app.UseSession();

    // Adiciona MVC ao pipeline da solicitação
    app.UseMvc();
}

Como é possível notar, no ASP.NET todos os middlewares são definidos no método Configure no padrão Use* seguindo do nome do middleware. Neste método também é possível adicionar middlewares customizados, utilizando os métodos Use ou Run. A diferença entres eles é que os middlewares definidos com Run são middlewares “finais”, após eles, nenhum outro middleware é chamado.

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

Entendendo a ordem do pipeline

Para compreender a ordem de execução dos middlewares no pipeline, vamos definir alguns middlewares simples:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Primeiro middleware (antes)");
        await next();
        await context.Response.WriteAsync("Primeiro middleware (depois)");
    });

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

Neste exemplo os middlewares estão sendo definidos como métodos anônimos, em um artigo futuro mostrarei as formas de declará-los.

Neste ponto o importante é que sabia que os middlewares definidos com Use recebem dois parâmetros: a instância de um objeto HttpContext e a instância do delegate RequestDelegate, que aponta para o próximo middleware no pipeline.

O resultado do código acima será algo assim:

Primeiro middleware (antes)
Segundo middleware (antes)
Middleware final
Segundo middleware (depois)
Primeiro middleware (depois)

Note que a ordem que esses middlewares estiverem definidos no método Configure irá indicar a ordem que eles serão chamados no pipeline. Ao chegar o último middleware do pipeline, os middlewares anteriores são chamados novamente.

Short-circuiting middleware

Caso algum dos middlewares não invocar o método next(), o pipeline será encerrado antes do seu final:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Primeiro middleware (antes)");
        await next();
        await context.Response.WriteAsync("Primeiro middleware (depois)");
    });

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        // await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

O resultado será:

Primeiro middleware (antes)
Segundo middleware (antes)
Segundo middleware (depois)
Primeiro middleware (depois)

Um middleware não final, que não invoca o método next, é chamado de short-circuiting middleware.

Dividindo o pipeline

Nos exemplos anteriores, o nosso pipeline possuía apenas um fluxo: o primeiro middleware sempre vem antes do segundo e por fim é chamado o middleware final. Mas não precisa ser sempre desta forma. Dependendo da solicitação pode ser definido um fluxo diferente para o pipeline.

Criando novo fluxo final

Com os métodos Map ou MapWhen é possível definir um novo fluxo final para o pipeline. O método Map permite especificar um middleware que será invocado de acordo com o caminho da solicitação. Já o MapWhen possui mais poder porque o padrão pode ser definido utilizando o objeto HttpContext:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Primeiro middleware (antes)");
        await next();
        await context.Response.WriteAsync("Primeiro middleware (depois)");
    });

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Map("/foo",
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /foo (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /foo (depois) ");
            });
    });

    app.MapWhen(context => context.Request.Path.StartsWithSegments("/bar"), 
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /bar (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /bar (depois) ");
            });
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

Mesmo sendo possível invocar o método next() nos middlewares definidos com Map e MapWhen, por não existir nenhum middleware seguinte, ao ser invocado é gerado um erro. Por exemplo, se a solicitação possuir um caminho /foo será gerado um fluxo assim:

Lembrando que também há a volta.

Se os métodos next() forem omitidos dos middlewares definidos em Map e MapWhen, teríamos um fluxo assim:

Primeiro middleware (antes)
Segundo middleware (antes)
Middleware para o caminho /foo (antes)
Middleware para o caminho /foo (depois)
Segundo middleware (depois)
Primeiro middleware (depois)

Criando um fluxo alternativo no pipeline

Para evitar o problema do uso do next() nos middlewares definidos em Map e MapWhen, é possível utilizar o método UseWhen, que funciona da mesma forma que o MapWhen, com a diferença que após executá-lo o fluxo do pipeline retorna ao caminho padrão:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Primeiro middleware (antes)");
        await next();
        await context.Response.WriteAsync("Primeiro middleware (depois)");
    });

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Map("/foo",
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /foo (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /foo (depois) ");
            });
    });

    app.UseWhen(context => context.Request.Path.StartsWithSegments("/bar"), 
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /bar (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /bar (depois) ");
            });
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

Caso a requisição tenha o caminho /bar, o resultado do pipeline será:

Primeiro middleware (antes)
Segundo middleware (antes)
Middleware para o caminho /bar (antes)
Middleware final
Middleware para o caminho /bar (depois)
Segundo middleware (depois)
Primeiro middleware (depois)

Podemos ilustrar isso na imagem abaixo:

Conclusão

Como é possível ver, a ideia por trás dos middlewares no ASP.NET Core é simples, porém muito poderosa. Muitos recursos do framework são disponibilizados para as aplicações através de middlewares.

Por isso o conhecimento do funcionamento do seu pipeline é um requisito obrigatório de todo desenvolvedor ASP.NET Core.

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

Otimização adaptativa no .NET Core 2.1

Ao desenvolver um sistema utilizando uma linguagem de alto nível, sempre teremos o custo do processo de compilação. Mesmo que não ganhe muito destaque, no final este será o processo que definirá a performance da aplicação.

Linguagens consideradas “mais rápidas”, como C, geram aplicações mais performáticas porque durante o processo de compilação estática são realizadas várias otimizações, visando o ambiente definido. Este é um dos motivos que em ambiente Unix drivers, bibliotecas e outras aplicações, as vezes precisam ser compiladas localmente antes de serem instaladas no sistema.

Em linguagens de alto nível, geralmente o código não é compilado diretamente para a linguagem de máquina. Ele é compilado para uma camada intermediária e dela para a linguagem de máquina. Como pode ser ilustrado pela arquitetura do .NET demostrado na imagem abaixo:

Neste tipo de arquitetura, esta camada intermediária as vezes recebe o nome de máquina virtual, já que pode emular um ambiente, o responsável por executar o código intermediário da aplciação, em qualquer sistema. Um exemplo clássico disso é a Java Virtual Machine (JVM).

E será esta máquina virtual que definirá a performance da aplicação. Quanto mais eficiente for, mais performáticas podem ser as aplicações desenvolvidas. Por esses motivos que existem máquinas virtuais, como a HHVM, que tem por objetivo otimizar as aplicações desenvolvidas em PHP (com o fim do suporte agora em 2018) e Hack.

Entrando no .NET, nele não há uma máquina virtual clara, mas como é possível ver na imagem da arquitetura acima, ele possui uma camada que é responsável por intermediar a execução do código intermediário em código de máquina.

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

CoreCLR

Ao desenvolvermos uma aplicação em qualquer linguagem suportada pelo .NET Core, antes dela ser executada, o código é convertido para uma linguagem intermediária (a MSIL, que traduzido literalmente significa linguagem intermediária), e o CoreCLR ficará responsável por converter este código IL em linguagem de máquina.

É esta camada que realiza todo o processo de otimização das aplicações .NET Core. Além que garantir que o código é seguro, não gera vazamento de memória, entre outros recursos. Geralmente ele tem que decidir se irá gerar um código de rápida inicialização, com pouca otimização, ou se irá implementar uma grande otimização, atrasando a sua inicialização.

Sendo um sucessor do CLR do .NET Framework, o CoreCLR tomava esta decisão uma vez, pois se o código de máquina de um método já estivesse gerado, este sempre seria utilizado quando este método fosse referenciado no código.

A primeira vista, otimizar todos os métodos parece ser a melhor opção, mas em alguns casos, o método pode ser pouco utilizado, que não compensa o custo de otimização.

Visando melhorar a performance das aplicações .NET Core, na versão 2.1 desta biblioteca, o CoreCLR foi motificado, adicionando o suporte a tiered compilation (compilação em camadas), também conhecida como otimização adaptativa.

Tiered Compilation

Na otimização adaptiva a máquina virtual tem mais opções. Ela pode gerar um código “sujo” de rápida inicialização, e se notar que o método é muito utilizado, posteriormente pode otimizá-lo para obter máxima performance.

Na implementação deste recurso no CoreCLR, isso significa que agora um método pode ter várias compilações que podem ser substituídas durante a execução da aplicação. Desta forma, pode-se obter por uma rápida inicialização ou por otimização:

  • Rápida inicialização: Quando uma aplicação é iniciada, ela aguarda o código intermediário a ser convertido em linguagem de máquina. A tiered compilation pede para ser gerada uma compilação inicial rapidamente, mesmo que seja necessário sacrificar a qualidade da otimização. Caso o método seja chamado frequentemente, um código mais otimizado é gerado em background, e ele substituí o código gerado inicialmente, para se obter mais performance.

  • Otimização: Em uma aplicação .NET típica, a maioria do código da biblioteca é gerado de imagens pré-compiladas. O que é muito bom para uma rápida inicialização, mas as imagens pré-compiladas possuem restrições de versões e instruções de CPU que limitam a otimização. Para cada método dessas imagens que são frequentemente chamados, a tiered compilation pede para ser gerada em background uma nova compilação mais otimizada, que substituirá a versão pré-compilada.

Resultados

Você pode estar se perguntando, o quão rápido uma aplicação pode ser com este tipo de compilação. Nos testes da equipe do .NET, eles obtiveram uma melhoria de uns 35%. Em alguns casos até mais, como é possível ver nos gráficos abaixo:

Em aplicações que utilizamos internamente aqui na TreinaWeb, obtivemos uma melhora de cerca de 40%.

Testando nas suas aplicações

No momento, este recurso está em fase beta. Caso esteja utilizando a última versão de preview do .NET Core 2.2, ele já está ativo por padrão, mas no .NET Core 2.1 é necessário ativá-lo no seu ambiente.

Caso queria ativar a tiered compilation para todas as suas aplicações, adicione nas variáveis de ambiente do seu sistema a variável abaixo:

COMPlus_TieredCompilation="1"

Para ativá-la em uma aplicação que esteja sendo desenvolvida, adicione no arquivo de configuração do projeto a tag TieredCompilation:

<PropertyGroup>
   <!-- outras definições -->

   <TieredCompilation>true</TieredCompilation>
</PropertyGroup>

Já para um projeto que que esteja rodando, você pode adicionar o atributo System.Runtime.TieredCompilation no arquivo configProperties dele:

{
    "runtimeOptions": {
        "configProperties": {
            "System.Runtime.TieredCompilation": true
        }
    },
    "framework": {
        ...
    }
}

Conclusão

A otimização adaptativa ainda está em fase beta no .NET Core 2.1, mas devido aos seus beneficios, é um recurso que vale a pena ser testado.

Caso encontre qualquer bug, não hesite em informar para a equipe do CoreCLR na sua página no git.

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