ASP .NET Core

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.

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.

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().

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().

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.

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.

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.

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.

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.

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.

Novos recursos do ASP.NET Core 2.1

No final do mês de maio deste ano, o .NET Core foi atualizado para a versão 2.1, e com ela, novos recursos foram adicionados na aplicação padrão do ASP.NET MVC. Para conhecê-los, vamos criar uma aplicação para esta versão.

Criando a aplicação

Crie a aplicação com o comando abaixo:

dotnet new mvc -n AspNetCore2_1Exemplo -au Individual

Ao executar o projeto, a primeira coisa que notará é que ele carregará duas URLs:

Uma HTTP e outra HTTPS. Seguindo as recomendações de que toda aplicação deve implementar SSL, no ASP.NET Core 2.1, o template padrão já adiciona o suporte a este protocolo na aplicação. Assim, ao acessá-la, sempre seremos redirecionados para a versão HTTPS:

Outro recurso adicionado no template padrão desta versão é a exibição da mensagem indicando o uso de cookie:

Isso é algo que segue a recomendação do GDPR.

Você pode adicionar este recurso em um projeto configurando o uso do middleware CookiePolicyMiddleware, que pode ser feito, adicionando a linha abaixo na configuração da sua aplicação:

app.UseCookiePolicy();

Para ela sempre sempre redirecionada para a versão HTTPS, adicione a linha abaixo:

app.UseHttpsRedirection();

Quanto à mensagem do uso de cookies, você pode alterá-la no arquivo Views/Shared/_CookieConsentPartial.cshtml:

@using Microsoft.AspNetCore.Http.Features

@{
    var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
    var showBanner = !consentFeature?.CanTrack ?? false;
    var cookieString = consentFeature?.CreateConsentCookie();
}

@if (showBanner)
{
    <nav id="cookieConsent" class="navbar navbar-default navbar-fixed-top" role="alert">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#cookieConsent .navbar-collapse">
                    <span class="sr-only">Toggle cookie consent banner</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <span class="navbar-brand"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></span>
            </div>
            <div class="collapse navbar-collapse">
                <p class="navbar-text">
                    Use this space to summarize your privacy and cookie use policy.
                </p>
                <div class="navbar-right">
                    <a asp-controller="Home" asp-action="Privacy" class="btn btn-info navbar-btn">Learn More</a>
                    <button type="button" class="btn btn-default navbar-btn" data-cookie-string="@cookieString">Accept</button>
                </div>
            </div>
        </div>
    </nav>
    <script>
        (function () {
            document.querySelector("#cookieConsent button[data-cookie-string]").addEventListener("click", function (el) {
                document.cookie = el.target.dataset.cookieString;
                document.querySelector("#cookieConsent").classList.add("hidden");
            }, false);
        })();
    </script>
}

Por fim, ao registrar um usuário e acessar a página de gerenciamento da conta, é possível ver o link da opção para exportar os dados e excluir a conta:

Isso também foi implementado devido às regras do GDPR. Também por causa dessas regras, é criada uma página onde deve ser adicionado a política de privacidade da aplicação:

Ela não aparece no menu, mas se encontra no caminho: Home/Privacy

Conclusão

O ASP.Net Core 2.1 adiciona uma série de recursos que facilitam a adequação da aplicação às mais novas regras da internet, reduzindo a necessidade de codificação e preocupação com estes detalhes, deixando o programador focado apenas com a lógica de negócio do projeto.

Utilizando o Micro ORM PetaPoco em uma aplicação ASP.NET Core

Como a maioria dos desenvolvedores .NET, quase todas as minhas aplicações fazem uso do Entity Framework. Fazer parte do ecossistema, ser integrado ao Visual Studio, possuir vários recursos, são sempre vantagens que nos leva a optar por seu uso.

Mas, as vezes, mesmo estas vantagens podem aborrecer. Pois quando que ser algo fora do padrão, o processo pode ser muito verboso ou caso o projeto seja muito simples, caímos na situação de matar uma barata com um canhão.

Para estes casos, felizmente, temos os micro-frameworks ORM. Em artigos passados, já falei sobre o Dapper e o ServiceStack.OrmLite.

No artigo de hoje abordarei o PetaPoco.

PetaPoco

O PetaPoco é um micro-framework ORM, criado por Brad Robinson, da topten software, que agora é mantido pelo grupo Collaborating Platypus.

Focado em rapidez e facilidade, o PetaPoco é uma solução de um arquivo, por isso é pequeno e não possui dependências. Como o nome sugere, trabalha com classes POCO, que requerem pouca configuração. Para facilitar, também inclui templates T4, que geram automaticamente classes POCO e suporta configurações Fluent.

No momento da criação deste artigo, ele suporta os bancos: SQL Server, SQL Server CE, Microsoft Access, SQLite, MySQL, MariaDB, Firebird e PostgreSQL (Oracle é suportado, mas não possui testes de integração).

Para exemplificar o seu uso, vamos ver um exemplo dele em uma aplicação ASP.NET Core.

Criando a aplicação

No terminal, digite o código abaixo para criar uma aplicação chamada AspNetCorePetaPoco:

dotnet new mvc -n AspNetCorePetaPoco

Acesse a pasta da aplicação e adicione nela o pacote do PetaPoco:

dotnet add package PetaPoco.Core.Compiled

Por fim, é necessário adicionar a biblioteca do provedor que iremos utilizar:

dotnet add package MySql.Data

Não esqueça de aplicar o restore:

dotnet restore

Agora podemos começar a configuração do PetaPoco.

Criando a classe POCO

Para esta entidade será utilizado a classe POCO (aka Model/Entidade) abaixo:

using System;

namespace AspNetCorePetaPoco.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Quantity { get; set; }
        public double Price { get; set; }
    }
}

Caso queria modificar o comportamento padrão, pode ser utilizado fluent, ou definir os atributos decoradores, por exemplo:

[TableName("Pessoas")]
[PrimaryKey("Id", AutoIncrement = false)]
public class Pessoa
{
    [Column]
    public Guid Id { get; set; }

    [Column]
    public string Nome { get; set; }

    [Column]
    public int Idade { get; set; }

    [Column]
    public int Altura { get; set; }

    [Column(Name = "Data Nascimento")]
    public DateTime? Nascimento { get; set; }

    [Ignore]
    public string NomeIdade => $"{Nome} tem a idade {Idade}";
}

Neste arquivo, me aterei a classe Product.

Configurando o acesso ao banco de dados

Assim como os outros micro-ORM, o PetaPoco não define uma classe de configuração, o acesso ao banco de dados pode ser obtido através do método DataBase da classe PetaPoco:

var db = new PetaPoco.Database("connectionStringName");

Ou através de fluent:

var db = DatabaseConfiguration.Build()
         .UsingConnectionString("Server=127.0.0.1;Uid=root;Pwd=1234;Database=PetaPocoExample;Port=3306")
         .UsingProvider<MySqlDatabaseProvider>()
         .UsingDefaultMapper<ConventionMapper>(m =>
         {
             m.InflectTableName = (inflector, s) => inflector.Pluralise(inflector.Underscore(s));
             m.InflectColumnName = (inflector, s) => inflector.Underscore(s);
         })
         .Create();

Ele também não cria as estruturas no banco de dados, então elas devem ser criadas “na mão”. Neste exemplo, estou definindo o código de criação da tabela em um arquivo chamado mysql_init.sql, que foi salvo em uma pasta chamada Scripts e contém o conteúdo abaixo:

CREATE TABLE IF NOT EXISTS Product (

    id              bigint AUTO_INCREMENT NOT NULL,
    name            varchar(500) NOT NULL,
    quantity        int NOT NULL,
    price           decimal(10,2) NOT NULL, 
    PRIMARY KEY (id)
) ENGINE=INNODB;

Agora, iremos definir a execução deste script e o acesso ao banco, em um repositório abstrato:

using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using PetaPoco;
using PetaPoco.Providers;

namespace AspNetCorePetaPoco.Repositories
{
    public abstract class AbstractRepository<T>
    {
        private string _connectionString;
        private IDatabase _db;
        protected IDatabase Db => _db;
        public AbstractRepository(IConfiguration configuration){
            _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");

            _db =  DatabaseConfiguration.Build()
                    .UsingConnectionString(_connectionString)
                    .UsingProvider<MySqlDatabaseProvider>()
                    .Create();

            _db.Execute(LoadTextResource("mysql_init.sql"));
        }
        public abstract void Add(T item);
        public abstract void Remove(int id);
        public abstract void Update(T item);
        public abstract T FindByID(int id);
        public abstract IEnumerable<T> FindAll();

        private string LoadTextResource(string name)
        {
            var path = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "Scripts", name);
            System.Console.WriteLine(path);
            System.IO.StreamReader r = new System.IO.StreamReader(path);
            string str = r.ReadToEnd();
            r.Close();

            return str;
        }
    }
}

Note que no método construtor um objeto de _Db foi criado:

_db =  DatabaseConfiguration.Build()
                    .UsingConnectionString(_connectionString)
                    .UsingProvider()
                    .Create();

Acima, é importante que o Provider reflita corretamente a biblioteca de acesso instalada.

Agora para finalizar a configuração, vamos definir o repositório abaixo:

using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using PetaPoco;
using PetaPoco.Providers;
using AspNetCorePetaPoco.Models;

namespace AspNetCorePetaPoco.Repositories
{
    public class ProductRepository: AbstractRepository
    {
        public ProductRepository(IConfiguration configuration): base(configuration) { }

        public override void Add(Product item)
        {
            Db.Insert(item);
        }
        public override void Remove(int id)
        {
            var product = Db.Single(id);
            Db.Delete(product);
        }
        public override void Update(Product item)
        {
            Db.Update(item);
        }
        public override Product FindByID(int id)
        { 
            return Db.Single(id);
        }
        public override IEnumerable FindAll()
        { 
            return Db.Query("SELECT * FROM Products");
        }
    }
}

Note que o PelaPoco gera as queries SQL com base na classe POCO informada no parâmetro dos métodos:

Db.Insert(item);

E quando ele não consegue gerar, deve ser informado a query:

return Db.Query("SELECT * FROM Products");

Usando o PetaPoco

Para exemplificar o uso do PetaPoco, crie o controller abaixo:

using System.Linq;
using AspNetCorePetaPoco.Models;
using AspNetCorePetaPoco.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace AspNetCorePetaPoco.Controllers
{
    public class ProductController : Controller
    {
        private readonly ProductRepository productRepository;

        public ProductController(IConfiguration configuration){
            productRepository = new ProductRepository(configuration);
        }

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

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

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

        // POST: Products/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind("Id,Name,Quantity,Price")] Product product)
        {
            if (ModelState.IsValid)
            {
                productRepository.Add(product);
                return RedirectToAction("Index");
            }

            return View(product);
        }

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

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

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

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

Também crie as views para as actions acima e então podemos ver o sistema funcionando:

Print da aplicação criada no artigo

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES