ASP .NET Core

Criando uma API RESTful com o Carter e .NET Core

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

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

C# (C Sharp) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
CONHEÇA O CURSO

Carter

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

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

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

Criando a aplicação para utilizar o Carter

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

dotnet new web -n CarterAPI

E adicionar o pacote carter:

dotnet add package carter

Em seguida ele deve ser configurado na classe Startup:

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

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

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

Também é possível instalar o CarterTemplate:

dotnet new -i CarterTemplate

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

dotnet new Carter -n CarterAPI

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

Criando um módulo

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

using Carter;

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

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

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

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

Criando o CRUD

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

Inicialmente defina uma entidade:

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

E um repositório:

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

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

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

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

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

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

Por fim, podemos definir os endpoints:

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

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

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

            repository.Edit(pessoa);

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

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

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

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

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

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

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

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

OpenAPI

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

Para os endpoints deste artigo, defini as classes abaixo:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            repository.Edit(pessoa);

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

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

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

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

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

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

Configurá-lo no projeto:

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

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

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

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

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

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

Conclusão

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

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

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

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.

Criando um middleware customizado para ASP.NET Core

Em um artigo anterior expliquei o pipeline do ASP.NET Core e o funcionamento dos middlewares neste tipo de aplicação. Nele apenas citei a possibilidade de criar middleware customizados. Como nesta semana necessitei realizar este procedimento, vou explicar neste artigo como isso pode ser feito.

Definindo o middleware customizado diretamente no pipeline

A forma mais simples de definir um middleware customizado é o adicionando diretamente no pipeline no método Configure da classe Startup:

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

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Olá Mundo!");
    });
}

Como expliquei antes, os códigos executados no pipeline são middlewares, então se adicionarmos instruções, mesmo que seja indicando o que está havendo, como a acima, elas serão consideradas um middleware.

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

Definindo uma classe “Middleware”

No dia a dia você irá definir middlewares mais complexos, então o ideal é defini-los em uma classe a parte:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsync("Chamou nosso middleware (antes)");
        await _next(httpContext);
        await httpContext.Response.WriteAsync("Chamou nosso middleware (depois)");
    }
}

A classe “Middleware” obrigatoriamente precisa ter a estrutura acima. Receber no construtor um objeto RequestDelegate e definir um método Invoke que recebe por parâmetro um objeto HttpContext.

Esta classe pode ser definida no pipeline utilizando o método UseMiddleware:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMiddleware<MyMiddleware>();
}

Mas o recomendado é definir um método de extensão para a interface IApplicationBuilder:

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

E utilizá-lo no Configure:

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

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Olá Mundo!");
    });
}

Este é o padrão das bibliotecas que definem middlewares.

Definindo configurações para o Middleware

Existem algumas formas de definir configurações do middleware. Uma forma muito utilizada é especificá-las no método ConfigureServices. Para isso, é necessário definir uma classe com os atributos que o middleware receberá:

public class MyMiddlewareOptions 
{
    public string BeforeMessage { get; set; } = "Chamou nosso middleware (antes)";
    public string AfterMessage { get; set; } = "Chamou nosso middleware (depois)";
}

É recomendado que se defina valores padrão para as propriedades. Assim, se novas configurações não forem definidas, os valores padrão serão utilizados.

Essas opções podem ser passadas para o middleware como uma instância da classe, ou através de uma Action<T>, que é a forma mais utilizada. Para definir isso, é necessário adicionar um método de extensão para a interface IServiceCollection:

public static class MyMiddlewareExtensions
{
    public static IServiceCollection AddMyMiddleware(this IServiceCollection service, Action<MyMiddlewareOptions> options = default)
    {
        options = options ?? (opts => { });

        service.Configure(options);
        return service;
    }

    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Isso adicionará as opções no sistema de injeção de dependências do ASP.NET, o que nos permite obtê-las no middleware através de um parâmetro do construtor da classe:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly MyMiddlewareOptions _options;

    public MyMiddleware(RequestDelegate next, IOptions<MyMiddlewareOptions> options)
    {
        _next = next;
        _options = options.Value;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsync(_options.BeforeMessage);
        await _next(httpContext);
        await httpContext.Response.WriteAsync(_options.AfterMessage);
    }
}

Agora o método AddMyMiddleware pode ser chamado no ConfigureServices e novas configurações podem ser indicadas:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMyMiddleware(options => {
        options.BeforeMessage = "Mensagem antes definida no ConfigureServices";
        options.AfterMessage = "Mensagem depois definida no ConfigureServices";
    });
}

Definindo as opções em uma classe, também é possível definir as configurações no arquivo appsettings.json:

{
  "MyMiddlewareOptionsSection": {
    "BeforeMessage": "Mensagem antes definida no appsettings",
    "AfterMessage": "Mensagem depois definida no appsettings"
  }
}

E indicá-las ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyMiddlewareOptions>(Configuration.GetSection("MyMiddlewareOptionsSection"));
}

Como o nome da sessão será informado, no arquivo appsettings.json, você poderia definir qualquer nome de sessão.

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

Com este tipo de configuração, mesmo se o middleware for adicionado mais de uma vez no pipeline, todas as versões utilizarão as mesmas configurações. Para especificar configurações para cada vez que ele for adicionado no pipeline, é necessário definir um método UseX que aceite essas configurações:

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }

    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder, Action<MyMiddlewareOptions> options = default)
    {
        var config = new MyMiddlewareOptions();
        options?.Invoke(config);
        return builder.UseMiddleware<MyMiddleware>(config);
    }
}

E modificar o construtor do middleware:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly MyMiddlewareOptions _options;

    public MyMiddleware(RequestDelegate next, MyMiddlewareOptions options)
    {
        _next = next;
        _options = options;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsync(_options.BeforeMessage);
        await _next(httpContext);
        await httpContext.Response.WriteAsync(_options.AfterMessage);
    }
}

Note que no lugar de aceitar um objeto de IOptions<T>, agora é aceito um objeto de MyMiddlewareOptions. Assim, o middleware pode ser adicionado duas vezes ao pipeline e receber configurações diferentes:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMyMiddleware(options => {
        options.BeforeMessage = "Primeira mensagem antes definida no Configure";
        options.AfterMessage = "Primeira mensagem depois definida no Configure";
    });

    app.UseMyMiddleware(options => {
        options.BeforeMessage = "Segunda mensagem antes definida no Configure";
        options.AfterMessage = "Segunda mensagem depois definida no Configure";
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

Caso queira utilizar os dois métodos, basta definir dois construtores no middleware, um aceitando objeto de IOptions<T> e outro aceitando objeto da sua classe de opções.

Conclusão

Neste exemplo exemplifiquei com um middleware simples as várias configurações disponíveis na criação de um middleware customizado. Caso trabalhe com aplicações ASP.NET, sempre que possível defina middleware customizados para manter um código limpo e reutilizar recursos.

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.

Criando um API Gateway com ASP.NET Core e Ocelot

Atualmente fala-se muito na criação de microsserviços, aplicações que executam uma função específica. Quando se cria um sistema baseado nesta arquitetura, teremos vários microsserviços executando funcionalidades específicas do sistema.

Como demostrado na imagem abaixo:

Assim, pode ocorrer situações onde é necessário obter informações de mais de um microsserviço, ou o sistema precisa ser consumido por vários clients. Com aplicações mobile, web, aplicações de terceiro, etc.

Nestes cenários, pode ser custoso ter que gerenciar o acesso de cada microsserviço do sistema. Cada um com as suas particularidades. É neste ponto que entra o API Gateway.

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

API Gateway

O API Gateway funciona como uma porta de entrada para o clients dos microsserviços do sistema. No lugar de chamar os microsserviços diretamente, os clients chamam a API Gateway, que redireciona a solicitação para o microsserviço apropriado. Quando o microsserviço retornar à solicitação, o API Gateway a retorna para o client.

Ou seja, ele funciona como uma camada intermediária entre os clients e os microsserviços. Como todas as solicitações irão passar por ele, o gateway pode modificar as solicitações recebidas e retornadas, o que nos fornece algumas vantagens:

  • Os microsserviços podem ser modificados sem se preocupar com os clients;
  • Os microsserviços podem se comunicar utilizando qualquer tipo de protocolo. O importante é o gateway implementar um protocolo que seja compreendido pelos clients;
  • O gateway pode implementar recursos que não impactam nos microsserviços, como autenticação, logging, SSL, balanceamento de carga, etc.

A imagem abaixo ilustra bem o funcionamento do API Gateway:

Ocelot

Ocelot é uma biblioteca que permite criar um API Gateway com o ASP.NET. Possuindo uma grande gama de funcionalidades, como:

  • Agregação de solicitações;
  • Roteamento;
  • Autenticação;
  • Cache;
  • Balanceamento de carga;
  • Log;
  • WebSockets;
  • Service Fabric.

Mesmo que seja voltado para aplicações .NET que estejam implementando uma arquitetura de microsserviços, o Ocelot pode ser utilizado como API Gateway de qualquer tipo de sistema que implemente esta arquitetura.

Para conhecê-lo melhor, vamos ver um exemplo prático.

Criando microsserviços de exemplo

Para exemplificar o uso do Ocelot, criarei dois microsserviços, que serão apenas aplicações ASP.NET Web API. Uma se chamará Pedido e a outra Catalogo. A única diferença dessas aplicações para a padrão criada pelo .NET é uma modificação no controller Values:

  • Pedido:
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "Item 1", "Item 2" };
}
  • Catalogo:
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "Produto 1", "Produto 2" };
}

Com isso, conseguiremos diferenciar cada uma.

Criando a API Gateway

Como dito , o Ocelot permite configurar uma aplicação ASP.NET para que se comporte como API Gateway. Assim, para criá-lo inicialmente é necessário criar uma aplicação ASP.NET.

Como a aplicação funcionará apenas como API Gateway, podemos criar uma “vazia”:

dotnet new web -n Gateway

Este é o procedimento recomendado, mas é possível definir o Ocelot em qualquer tipo de aplicação ASP.NET.

Após a criação da aplicação é necessário adicionar a referência do Ocelot:

dotnet add package Ocelot

As configurações do Ocelot devem ser definidas em um arquivo JSON. Assim, vamos alterar a classe Program para que carregue este arquivo no início da execução da aplicação:

public class Program
{
    //Código omitido

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration(ic => ic.AddJsonFile("configuration.json"))
            .UseStartup<Startup>();
}

Veremos este arquivo de configuração à frente. Antes, iremos alterar a classe Startup.

Nela iremos habilitar o serviço e o middleware do Ocelot:

public class Startup
{
    private readonly IConfiguration _configuration;

    public Startup(IConfiguration configuration)
    {
        this._configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOcelot(_configuration);
    }

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

        app.UseOcelot().Wait();
    }
}

Como o arquivo de configuração do Ocelot está sendo carregado pelo sistema de configuração do ASP.NET, uma referência dele é passada para o serviço Ocelot:

services.AddOcelot(_configuration);

Já no método Configure, o middleware do Ocelot é adicionado ao pipeline de execução do ASP.NET:

app.UseOcelot().Wait();

É importante que esta sempre seja a última linha do método Configure. Pois o middleware do Ocelot é terminal. Ou seja, ele não invoca nenhum outro middleware do pipeline. Caso a solicitação realizada não seja encontrada nas configurações do Ocelot, ele gera um erro 404.

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

Configurando a API Gateway

O arquivo de configuração do Ocelot é composto de dois atributos:

{
    "ReRoutes": [],
    "GlobalConfiguration": {}
}

Em ReRoutes definimos como funcionará o sistema de redirecionamento da API Gateway. Já GlobalConfiguration definimos configurações globais que sobrescrevem configurações do ReRoutes.

Já no ReRoutes é possível definir uma série de funcionalidades, mas os pontos mais importantes são:

  • DownstreamPathTemplate: Define a URL que será utilizada na criação da solicitação para o microsserviço;
  • DownstreamScheme: Define o scheme utilizado na solicitação para o microsserviço;
  • DownstreamHostAndPorts: Define o Host e a porta (Port) utilizada na solicitação para o microsserviço;
  • UpstreamPathTemplate: Define a URL que o Ocelot irá utilizar para indicar que deve ser chamado o microsserviço definido nos atributos Downstream
  • UpstreamHttpMethod: Define os métodos HTTP aceitos;

Com isso em mente, podemos definir a seguinte configuração para a nossa API Gateway:

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "pedido",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/pedido/",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete", "Options" ]
    },
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "pedido",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/pedido/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete", "Options" ]
    },
    {
      "DownstreamPathTemplate": "/",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "catalogo",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/catalogo/",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete", "Options" ]
    },
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "catalogo",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/catalogo/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete", "Options" ]
    }
  ],
  "GlobalConfiguration": { }
}

Como os microsserviços serão executados em containers, o Ocelot consegue redirecionar as solicitações com base no nome dos containers. Assim, os hosts definidos no arquivo acima utilizam o nome dos containers.

Mas é possível definir um host “qualquer”, pelo IP, como:

"DownstreamHostAndPorts": [
  {
    "Host": "189.0.0.1",
    "Port": 80,
  }
],

Ou até pela URL:

"DownstreamHostAndPorts": [
  {
    "Host": "www.servidor.com",
    "Port": 80,
  }
],

Rodando

Como estamos lidando com microsserviços, o ideal é que eles sejam executados em containers (esta é a recomendação da Microsoft) e como isso não é o foco do artigo, aqui não explicarei a configuração dos containers, mas você pode vê-lo no repositório do projeto aqui

Ao executar as aplicações através do docker composer, conseguiremos acessar cada microsserviço através da API Gateway:

Ou mesmo diretamente (devido a configuração realizada nos containers):

Concluindo

Caso pretenda criar uma aplicação que implemente o padrão de microsserviços, a criação de uma API Gateway é quase uma tarefa obrigatória. No caso de aplicações .NET, o Ocelot facilita esta criação.

Então caso esteja interessado nesta biblioteca, recomendo a leitura da sua documentação, aqui, pois ela possui uma série de recursos que não foram abordados aqui.

Por hoje é só, até a próxima 🙂

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.

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