ASP.NET

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

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.

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>

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

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.

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.

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 🙂

Criando um Web Service com o ServiceStack – Parte 1

No artigo passado conhecemos o ServiceStack e vimos um exemplo do ServiceStack.OrmLite. Como comentado no artigo anterior, o ServiceStack possui uma gama de recursos, criados para substituir os frameworks WCF, Web API, ASP.NET MVC.

Assim, continuando o seu estudo, hoje veremos como criar uma aplicação REST simples em ASP.NET Core.

Para este artigo a aplicação terá apenas rota que irá retornar o que o usuário indicar. No próximo iremos integrá-la ao ServiceStack.OrmLite.

Entendo o projeto do ServiceStack

Para o Visual Studio, o ServiceStack possui um template (pode ser baixado aqui) que facilita a criação do web service.

Ao instalar este template e criar uma aplicação ServiceStack ASP.NET Empty, será criada uma solução com 4 projetos:

  • Projeto: Projeto ASP.NET vazio. É o projeto “host”, onde deve ser adicionado os recursos web, como: views, arquivos js, css, imagens, fontes, etc;
  • Projeto.ServiceInterface: Projeto que contém os serviços e a camada de negócio da aplicação.
  • Projeto.ServiceModel: Projeto que contém as classes DTO da aplicação. Essas classes definem os “contratos” dos services.
  • Projeto.Test: Projeto que contém os testes de unidade da aplicação.

Como o projeto deste artigo será criado com o .NET Core em um ambiente Unix, ele não contém múltiplos projetos, apenas pastas para separar as camadas ServiceInterface, e ServiceModel.

Então vamos colocar a mão na massa!

Colocando a mão na massa

No o .NET Core instalado na máquina, no terminal crie uma aplicação ASP.NET Core vazia com o comando abaixo:

dotnet new web -n ServiceStackExample

Acesse a pasta do projeto criado e adicione nele a dependência do ServiceStack:

dotnet add package ServiceStack.Core

Pronto, o nosso projeto estará criado e com o ServiceStack referenciado.

Criando as classes DTO

Dentro do projeto, crie uma pasta chamada ServiceModel e dentro dela crie uma classe chamada CategoryRespose, contendo o código abaixo:

using ServiceStack;

namespace ServiceStackExample.ServiceModel {
    public class CategoryResponse
    {
        public string Result { get; set; }

        public ResponseStatus ResponseStatus { get; set; }
    }
}

Como o nome indica, esta classe será responsável pela resposta do web service. Ela só necessita de uma propriedade Result, mas para que exceções também sejam exibidas no navegador, é necessário adicionar propriedade ResponseStatus.

Agora dentro da mesma pasta adicione uma classe Category, contendo o código abaixo:

using ServiceStack;

namespace ServiceStackExample.ServiceModel {

    [Route("/categories")]
    [Route("/categories/{Name}")]
    public class Category
    {
        public string Name { get; set;}
    }
}

Esta classe irá tratar as solicitações do web service para as URLs definidas nela:

[Route("/categories")]
[Route("/categories/{Name}")]

O valor {Name} definido na última URL acima, será atribuído a propriedade Name da classe.

Criando a classe Service

Agora crie no projeto a pasta ServiceInterface, e a ela adicione uma classe chamada CategoryService, contendo o código abaixo:

using ServiceStack;
using ServiceStackExample.ServiceModel;

namespace ServiceStackExample.ServiceInterface {
    public class CategoryService: Service
    {
        public object Any(Category  request){
            return new CategoryResponse { Result = $"Categoria: {request.Name}" };
        }
    }
}

Como o nome indica, esta classe é a implementação do “serviço” das classes que definimos na pasta ServiceModel. Nela pode ser definidos métodos para tratar os métodos do HTTP: GET, POST, PUT, DELETE, etc; Esses métodos devem ter o mesmo nome dos métodos do HTTP.

Ou seja, para criar um método que será chamado quando houver uma solicitação GET, o método deve ser nomeado como Get. Caso queira tratar uma solicitação POST, o método deve ser nomeado como Post; e assim por diante.

Também pode ser definido o método Any que é para qualquer solicitação, que é o que foi feito na classe acima.

Iniciando o serviço

Para que o serviço seja iniciado e as solicitações que definimos com o ServiceStack sejam tratadas pela aplicação, temos que criar uma classe AppHost, contendo o conteúdo abaixo:

using Funq;
using ServiceStack;

namespace ServiceStackExample
{
   public class AppHost: AppHostBase {
       public AppHost(): base("Treinaweb web Services", typeof(ServiceInterface.CategoryService).Assembly){}

        public override void Configure(Container container)
        {    
        }
    }
}

A classe acima está apenas registrando o nosso serviço, mas no seu método configure pode ser configurado outros recursos, como o ServiceStatck.OrmLite, Cache, Redis, Autenticação, etc;

Por fim, esta classe precisa ser chamada no método Configure da classe Startup:

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

Pronto, o nosso serviço já está criado e configurado e podemos testá-lo.

Executando o serviço

No terminal execute a aplicação:

dotnet run

Aí no navegador acesse o serviço: http://localhost:5000/categories/Mouse:

Print do exemplo do Service Stack Funcionando.

Tudo certo. No próximo artigo trabalharemos mais a aplicação.

Até a próxima!

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES