ASP.NET

Enviando e-mails no ASP.NET Core

Todo sistema precisa notificar os usuários de algumas ações, sejam notificações visuais na aplicação ou fora dela. Se tratando de notificações fora do sistema, a forma mais comum de realizar isso é através do envio de e-mails.

Para realizar este processo, o .NET possui o namespace System.Net.Mail. Mesmo as classes dele não sendo complexas, utilizá-las requer uma série de configurações, algo que pode ser facilitado com o uso da biblioteca Coravel.

Coravel

Coravel é uma biblioteca .NET Core open-source que permite a implementação de agendamento de tarefas, caching, mensageria e envio de e-mails de forma simples e com pouca configuração.

Ela tem por objetivo ser intuitiva e direta ao ponto, para que o desenvolvedor possa focar nos pontos mais importantes da aplicação. Mesmo a biblioteca fornecendo vários recursos, vamos focar apenas na parte de envio de e-mails, que nos permite:

  • Utilizar Razor templates;
  • Visualizar um preview;
  • Definir as configurações no appsetings.json;
  • Enviar os e-mails para um arquivo de log para testes;
  • Entre outras opções.

Criando a aplicação

Para exemplificar o uso da biblioteca, crie uma aplicação ASP.NET Core:

dotnet new mvc --auth Individual -n exemploenvioemail

E adicione nela o pacote Coravel.Mailer:

dotnet add package Coravel.Mailer

Agora poderemos definir o envio de e-mail.

Definindo o tipo do e-mail

O módulo de e-mail da Coravel utiliza “Mailables” para enviar um e-mail. “Mailable” são classes que herdam Coravel.Mailer.Mail.Mailable e representam o tipo de e-mail que será enviado, como: “Novo usuário”, “Pedido realizado”, etc.

Neste artigo veremos o envio de e-mail quando um usuário for registrado, assim, iremos definir a classe abaixo:

using Coravel.Mailer.Mail;
using Microsoft.AspNetCore.Identity;

namespace ExemploEnvioEmail.Mailables
{
    public class NewUserMailable: Mailable<NewUserMailableViewModel>
    {
        private NewUserMailableViewModel _newUser;

        public NewUserMailable(NewUserMailableViewModel newUser) => this._newUser = newUser;

        public override void Build()
        {
            this.To(this._newUser)
                .From(new MailRecipient("contato@treinaweb.com.br", "Treinaweb Cursos"))
                .View("~/Views/Mail/NewUser.cshtml", this._newUser);
        }
    }
}

Nela, note que o e-mail é definido no método Build, onde em:

this.To(this._newUser)

É informado o remetente. Ele pode ser indicado como uma string:

To("mail@remente.com.br")

Como um objeto MailRecipient:

To(new MailRecipient("mail@remente.com.br", "Remente")

Ou como um objeto qualquer:

this.To(this._newUser)

Neste caso, o objeto precisa possuir uma propriedade pública chamada Email. Caso também tenha uma propriedade chamada Name, ela será utilizada como rótulo do e-mail. O nosso módulo possui essas propriedades:

public class NewUserMailableViewModel
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string CallbackUrl { get; set; }
}

Este mesmo padrão pode ser aplicado com o From:

.From(new MailRecipient("contato@treinaweb.com.br", "Treinaweb Cursos"))

Por fim, é informado o template do e-mail:

.View("~/Views/Mail/NewUser.cshtml", this._user);

Também é possível definir HTML puro utilizando o método Html:

public override void Build()
{
    this.To(this._user)
        .From(new MailRecipient("contato@treinaweb.com.br", "Treinaweb Cursos"))
        .Html("HTML");
}

Para que isso seja feito, Mailable precisa definir o tipo string ( Mailable<string>), se não será gerado um erro.

Não é utilizado neste exemplo, mas também há os métodos:

  • Cc (cópia);
  • Bcc (cópia oculta);
  • ReplyTo (remente de resposta).

Que aceitam uma coleção de string ou MailRecipient.

Também é importante frisar que o nome da “Mailable” será utilizado para definir o assunto do e-mail. Como os controllers do ASP.NET Core. A biblioteca remove "Mailable" do nome e define o assunto com base nas demais palavras capitalizadas. Por exemplo, para a nossa classe (NewUserMailable), o assunto do e-mail será “New User”. Entretanto é possível mudar este comportamento informando o assunto do e-mail no método Subject:

this.To(this._newUser)
    .From(new MailRecipient("contato@treinaweb.com.br", "Treinaweb Cursos"))
    .Subject($"Bem vindo {this._newUser.Name}")
    .View("~/Views/Mail/NewUser.cshtml", this._newUser);

Mas para este artigo, iremos seguir o padrão da biblioteca.

Criando o template do e-mail

Como vimos no tópico acima, o template do e-mail pode ser um arquivo Razor, definido no método View. Este arquivo segue o mesmo padrão de uma View:

@model ExemploEnvioEmail.Models.NewUserMailableViewModel

@{
   Layout = null;
}

<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <table border="0" cellpadding="0" cellspacing="0" width="100%">
        <tr>
        <td align="center">
            <table border="0" cellpadding="0" cellspacing="0" width="100%">
                <tr>
                    <td align="center" valign="top">
                      <h1>Bem vindo: @Model.Name</h1>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
    <tr>
        <td align="center">
            <table border="0" cellpadding="0" cellspacing="0" width="100%" >
              <tr>
                <td>
                    <p>
                        Bem vindo @Model.Name!
                    </p>
                    <p>
                        Estamos felizes que agora você faz parte da nossa comunidade. Por favor, clique <a href="@Html.DisplayFor(m => m.CallbackUrl)">aqui</a> para confirmar seu e-mail. Ou copie e cole no navegador o link abaixo:
                    </p>
                    <p>
                        <a href="@Html.DisplayFor(m => m.CallbackUrl)">@Html.DisplayFor(m => m.CallbackUrl)}</a>
                    </p>
                </td>
              </tr>
            </table>
        </td>
    </tr>
    <tr>
        <td align="center">
            <table border="0" cellpadding="0" cellspacing="0" width="100%">
              <tr>
                <td align="left">
                  <p>    
                    <a href="https://www.treinaweb.com.br">Treinaweb</a> | <a href="https://www.treinaweb.com.br/blog">Treinaweb Blog</a>
                </td>
              </tr>
            </table>
        </td>
    </tr>
    </table>
</body>
</html>

Note que no início do arquivo, indicamos o model:

@model ExemploEnvioEmail.Models.NewUserMailableViewModel

E que não deve ser utilizado o layout padrão da aplicação:

@{
   Layout = null;
}

Por causa disso, no arquivo é definida uma página HTML completa. À frente veremos o uso de um layout.

Com o nosso “Mailable” e View/Template definidos, podemos configurar o módulo de e-mail da Coravel na nossa aplicação.

Configurando o Coravel.Mailer

Para configurar a Coravel.Mailer, é necessário adicionar o seu serviço no método ConfigureServices:

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

    //Habilitando o Coravel.Mailer
    services.AddMailer(this.Configuration);
}

E no arquivo appsettings.json é necessário indicar qual será o Driver utilizado para enviar os e-mails. Pode ser indicado um servidor SMTP:

"Coravel": {
  "Mail": {
    "Driver": "SMTP",
    "Host": "smtp.mail.to",
    "Port": 2525,
    "Username": "usuario",
    "Password": "senha"
  }
}

Ou um arquivo de log:

"Coravel": {
  "Mail": {
    "Driver": "FileLog"
  }
}

Neste caso, os e-mails serão enviados para um arquivo chamado mail.log, criado na raiz do projeto. Este tipo de configuração é ideal durante o desenvolvimento e testes da aplicação. E será ele que utilizaremos neste artigo.

Por fim, só é necessário enviar o e-mail.

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

Enviando e-mails com Coravel.Mailer

O serviço do módulo de e-mail da Coravel adiciona nas dependências da aplicação uma instância de Coravel.Mailer.Mail.IMailer, desta forma, podemos definir que ela será recebida no construtor do controller:

private readonly IMailer _mailer;

public AccountController(
    /* Código omitido */
    IMailer mailer)
{
    /* Código omitido */
    _mailer = mailer;

}

Com isso, em qualquer action dele, um e-mail pode ser enviado com o método SendAsync:

public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
    /* Código omitido */
    await this._mailer.SendAsync(new NewUserMailable(
        new NewUserMailableViewModel {
            Name = user.UserName,
            Email = user.Email,
            CallbackUrl = callbackUrl
        }
    ));

    /* Código omitido */
}

Agora, sempre que um usuário for registrado na aplicação:

Cadastro de um usuário

O e-mail enviado será salvo no arquivo mail.log:

---------------------------------------------
Subject: New User
To: mail@teste.com.br <mail@teste.com.br>    
From: Treinaweb Cursos <contato@treinaweb.com.br>
ReplyTo: 
Cc: 
Bcc: 
---------------------------------------------



<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <table border="0" cellpadding="0" cellspacing="0" width="100%">
        <tr>
        <td align="center">
            <table border="0" cellpadding="0" cellspacing="0" width="100%">
                <tr>
                    <td align="center" valign="top">
                      <h1>Bem vindo: mail@teste.com.br</h1>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
    <tr>
        <td align="center">
            <table border="0" cellpadding="0" cellspacing="0" width="100%" >
              <tr>
                <td>
                    <p>
                        Bem vindo mail@teste.com.br!
                    </p>
                    <p>
                        Estamos felizes que agora você faz parte da nossa comunidade. Por favor, clique <a href="https://localhost:5001/Account/ConfirmEmail?userId=3a3c71fd-eee3-44c5-b309-4cd55fb2a4df&code=CfDJ8BvCiBo0fQdGsoZDKA5HQm9WOefgiQKTgLo5rDFLbeUAOOFCIfiApXKu%2F6crEeW7wdrgz9h5Z0Y15NdZivlzQ4duSNFcOmbSObG0fdMbGTQ%2ByvC02wR1Av9QFxL%2Fkc5Vg0nwVtsrttBmAeFMHZSxnokR1n0rI9rUyzUpxAYywdpPJOhHW2r6DsSdK0VWp2u6Xe1uWsLDXmSMeMu96g8yZNz18N1SCPHgjNrU8eJdK1wP4Q5vUj%2Bdn21epFqNjaM5Fw%3D%3D">aqui</a> para confirmar seu e-mail. Ou copie e cole no navegador o link abaixo:
                    </p>
                    <p>
                        <a href="https://localhost:5001/Account/ConfirmEmail?userId=3a3c71fd-eee3-44c5-b309-4cd55fb2a4df&code=CfDJ8BvCiBo0fQdGsoZDKA5HQm9WOefgiQKTgLo5rDFLbeUAOOFCIfiApXKu%2F6crEeW7wdrgz9h5Z0Y15NdZivlzQ4duSNFcOmbSObG0fdMbGTQ%2ByvC02wR1Av9QFxL%2Fkc5Vg0nwVtsrttBmAeFMHZSxnokR1n0rI9rUyzUpxAYywdpPJOhHW2r6DsSdK0VWp2u6Xe1uWsLDXmSMeMu96g8yZNz18N1SCPHgjNrU8eJdK1wP4Q5vUj%2Bdn21epFqNjaM5Fw%3D%3D">https://localhost:5001/Account/ConfirmEmail?userId=3a3c71fd-eee3-44c5-b309-4cd55fb2a4df&code=CfDJ8BvCiBo0fQdGsoZDKA5HQm9WOefgiQKTgLo5rDFLbeUAOOFCIfiApXKu%2F6crEeW7wdrgz9h5Z0Y15NdZivlzQ4duSNFcOmbSObG0fdMbGTQ%2ByvC02wR1Av9QFxL%2Fkc5Vg0nwVtsrttBmAeFMHZSxnokR1n0rI9rUyzUpxAYywdpPJOhHW2r6DsSdK0VWp2u6Xe1uWsLDXmSMeMu96g8yZNz18N1SCPHgjNrU8eJdK1wP4Q5vUj%2Bdn21epFqNjaM5Fw%3D%3D</a>
                    </p>
                </td>
              </tr>
            </table>
        </td>
    </tr>
    <tr>
        <td align="center">
            <table border="0" cellpadding="0" cellspacing="0" width="100%">
              <tr>
                <td align="left">
                  <p>    
                    <a href="https://www.treinaweb.com.br">Treinaweb</a> | <a href="https://www.treinaweb.com.br/blog">Treinaweb Blog</a>
                </td>
              </tr>
            </table>
        </td>
    </tr>
    </table>
</body>
</html>

Coravel CLI

Outro recurso da Coravel que não citei é a sua ferramenta de linha de comando, o Coravel CLI. Esta ferramenta é uma global tool do .NET Core que pode ser instalada com o comando abaixo:

dotnet tool install --global coravel-cli

Entre os recursos que ela fornece, há a possibilidade de adicionar ao projeto o módulo de e-mail da biblioteca com o comando abaixo:

coravel mail install

Ele irá adicionar ao projeto a referência Coravel.Mailer e os arquivos:

  • ~/Views/Mail/_ViewStart.cshtml: Configuração que permite que as Views utilizem os templates da Coravel;
  • ~/Views/Mail/_ViewImports.cshtml: Configuração que permite o uso dos componentes definidos pela biblioteca;
  • ~/Views/Mail/Example.cshtml: Exemplo de uma View
  • ~/Mailables/Example.cs: Exemplo de um “Mailable”

Mesmo que a biblioteca Coravel.Mailer já esteja configurada na nossa aplicação, execute o comando acima. Ao fazer isso, poderemos alterar a nossa View para:

@model ExemploEnvioEmail.Models.NewUserMailableViewModel

@{
   ViewBag.Heading = "Bem vindo: " + Model.Name;
   ViewBag.Preview = "Obrigado por se registrar no nosso sistema";
}

<p>
    Bem vindo @Model.Name!
</p>
<p>
    Estamos felizes que agora você faz parte da nossa comunidade. Por favor, clique no botão abaixo para confirmar seu registro.
    @await Component.InvokeAsync("EmailLinkButton", new  { text = "Confirmar", url = Model.CallbackUrl })
</p>
<p>
    Ou copie e cole no navegador o link abaixo:
</p>
<p>
    <a href="@Html.DisplayFor(m => m.CallbackUrl)">@Html.DisplayFor(m => m.CallbackUrl)</a>
</p>
@section links
{
    <a href="https://www.treinaweb.com.br">Treinaweb</a> | <a href="https://www.treinaweb.com.br/blog">Treinaweb blog</a>
}

Como agora ela está utilizando o template da Coravel, foi possível definir algumas configurações:

  • ViewBag.Heading: Título do e-mail;
  • ViewBag.Preview: Preview do e-mail, para aplicações que fornecem isso;
  • section links: Links exibidos no rodapé do e-mail.

Não foi aplicado no exemplo acima, mas o template da biblioteca também possui a configuração ViewBag.Footer (ou section footer), para a definição do rodapé. Também é possível definir no arquivo de appsetting.json, os atributos:

"Coravel": {
    "Mail": {
        /* Logo da empresa  */
        "LogoSrc": "URL",

        /* Endereço, exibido no rodapé dos e-mails */
        "CompanyAddress": "RUA",

        /* Nome da empresa, exibido no rodaroé dos e-mails */
        "CompanyName": "Empresa",

        /* Cor utilizada no cabeçalho dos e-mails */
        "PrimaryColor": "#FFFFFF"
    }
}

Quando informados, esses atributos serão aplicados em todos os e-mails.

Conclusão

O módulo nativo de envio de e-mails do .NET é bem completo e eficiente, entretanto, a Coravel também fornece um módulo tão bom quanto, além de ser mais simples e cheio de recursos.

Devido a sua facilidade de configuração, sempre que necessitar enviar e-mails na sua aplicação, recomendo que verifique se esta biblioteca atende as suas necessidades. Pode ter certeza que a sua implementação será mais rápida que a implementação do módulo de e-mails do .NET.

Você pode ver o código completo da aplicação no meu GitHub.

Utilizando o hybrid ORM RepoDb em uma aplicação ASP.NET Core

No ambiente .NET, quando uma aplicação necessita persistir dados, geralmente fará uso de algum framework ORM. Se for algo simples tende a optar por um micro-ORM, como o Dapper e se for complexo, a opção é um “full-ORM”, como o Entity Framework. Mas quando a aplicação estiver no meio termo? É neste ponto que entra o hybrid ORM RepoDb.

RepoDb

O RepoDb é um framework ORM open-source que tem por objetivo sanar a brecha que há entre um micro-ORM e um macro-ORM (full-ORM). Fornecendo recursos que permitem o desenvolvedor alterar de forma simples entre operações básicas e avançadas.

Além de fornecer as operações CRUD padrão (criação, leitura, alteração e exclusão), também disponibiliza recursos avançados como: 2nd-Layer Cache, Tracing, Repositories e operações em lote (Batch/Bulk). Permitindo que seja utilizado tanto em um banco de dados simples quanto nos mais complexos.

Criado por Michael Pendon, o RepoDb se define como a melhor alternativa de ORM para o Dapper e o Entity Framework e procura ter a mesma adoção de ambos. Para ajudá-lo nisso, vamos conhecer este framework através de um exemplo.

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

Criando a aplicação

Como estou utilizando o Visual Studio Code, criarei a aplicação por linha de comando:

dotnet new mvc -n AspNetCoreRepodb

No momento da criação deste artigo, o RepoDb suporta os seguintes banco de dados:

  • SqlServer: RepoDb.SqlServer;
  • SqLite: RepoDb.SqLite;
  • MySql: RepoDb.MySql;
  • PostgreSql: RepoDb.PostgreSql.

Para o exemplo deste artigo irei utilizar o SQLite, desta forma é necessário adicionar a dependência abaixo:

dotnet add package RepoDb.SqLite

Não se esqueça de aplicar o restore no projeto:

dotnet restore

Com isso já podemos começar a nossa configuração do RepoDb.

Criando o model e tabela

Para este exemplo será utilizada a entidade abaixo:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

Como o RepoDb não realiza este procedimento, também é necessário criar a tabela desta entidade:

CREATE TABLE IF NOT EXISTS [Product]
(
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    Name TEXT,
    Quantity INTEGER,
    Price REAL
);

Agora podemos configurar o acesso ao banco.

Configurando o acesso ao banco de dados

Assim como o Dapper e diferente do Entity Framework, o RepoDb não fornece uma classe de configuração, o banco deve ser acessado via SqlConnection. Desta forma, para organizar o nosso código, irei implementar o padrão repository:

public interface IRepository<T>
{
    string ConnectionString { get; }
    void Add(T item);
    void Remove(int id);
    void Update(T item);
    T FindByID(int id);
    IEnumerable<T> FindAll();
}

Esta interface será implementada pela classe ProductRepository:

public class ProductRepository : IRepository<Product>
{
    private string _connectionString;
    public string ConnectionString => _connectionString;

    public ProductRepository(IConfiguration configuration)
    {
        _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
        RepoDb.SqLiteBootstrap.Initialize();
    } 

    public void Add(Product item)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            var id = dbConnection.Insert<Product, int>(item);
        }
    }

    public IEnumerable<Product> FindAll()
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            return dbConnection.ExecuteQuery<Product>("SELECT * FROM Product");
        }
    }

    public Product FindByID(int id)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            return dbConnection.Query<Product>(e => e.Id == id).FirstOrDefault();
        }
    }

    public void Remove(int id)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Delete<Product>(id);
            //Também poderia ser
            // dbConnection.Delete<Product>(e => e.Id = id);
        }
    }

    public void Update(Product item)
    {
        using (var dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Merge(item);
        }
    }
}

Note que no construtor da classe é chamado o bootstrapper do RepoDb para o SQLite:

public ProductRepository(IConfiguration configuration)
{
    _connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
    RepoDb.SqLiteBootstrap.Initialize();
} 

Isso é necessário para configurar o DataMapping da biblioteca para este banco de dados.

Caso trabalhe apenas com SQL RAW, como no exemplo do método FindAll:

public IEnumerable<Product> FindAll()
{
    using (var dbConnection = new SQLiteConnection(ConnectionString))
    {
        return dbConnection.ExecuteQuery<Product>("SELECT * FROM Product");
    }
}

Não é necessário utilizar o bootstrapper, pois esta é a forma mais simples de fazer uso da biblioteca, o que o torna muito semelhante ao Dapper. Mas o seu principal poder é visto quando definimos as ações via Fluent:

public void Add(Product item)
{
    using (var dbConnection = new SQLiteConnection(ConnectionString))
    {
        var id = dbConnection.Insert<Product, int>(item);
    }
}

E para o Fluent funcionar, é necessário que o RepoDb seja “inicializado” para o banco de dados em questão.

Além das ações implementadas nesta classe, também é possível realizar uma ação em lote, como a inserção:

using (var dbConnection = new SQLiteConnection(ConnectionString))
{
    dbConnection.InsertAll<Product>(products, batchSize: 100);
}

Note que é informado no parâmetro batchSize a quantidade de registros serão salvos. Após serem salvos, os id gerados serão atribuídos ao itens da lista.

Com o repositório criado, podemos definir o controller e as views para testar a nossa conexão.

Testando o RepoDb

Para testar, criaremos o controller abaixo:

public class ProductController : Controller
{
    private readonly IRepository<Product> productRepository;

    public ProductController(IRepository<Product> repository) 
        => productRepository = repository;

    // 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
    [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");
    }
}

Note que o repositório é recebido por parâmetro no construtor:

public ProductController(IRepository<Product> repository) 
    => productRepository = repository;

Por causa disso, vamos adicioná-lo via injeção de dependência:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddTransient<IRepository<Product>, ProductRepository>();
}

Ao definir as views, poderemos ver o sistema funcionando:

Tabela exibido dois produtos: Teclado e Mouse; com os preços 123,40 e 49,9; e as quantidades 10 e 20

Implementando o padrão Repository com o RepoDb

No nosso exemplo acima, o padrão repository foi implementado “manualmente”. Entretanto, o RepoDb já fornece uma implementação deste padrão. Para utilizá-lo basta herdar a classe BaseRepository, onde deve ser informado a entidade e o tipo de conexão.

Para que a nossa aplicação não necessite de muitas alterações, além desta classe, basta manter a interface IRepository que definimos:

public class ProductRepository : BaseRepository<Product, SQLiteConnection>, IRepository<Product>
{
    public ProductRepository(IConfiguration configuration) : base(configuration.GetValue<string>("DBInfo:ConnectionString"))
    {
        RepoDb.SqLiteBootstrap.Initialize();
    } 

    public void Add(Product item)
    {
        Insert<int>(item);
    }

    public IEnumerable<Product> FindAll()
    {
        return QueryAll();
    }

    public Product FindByID(int id)
    {
        return Query(id).FirstOrDefault();
    }

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

    public void Update(Product item)
    {
        Update(item);
    }
}

Note que o código da nossa classe ficou mais “limpo”. Caso execute a aplicação novamente, verá que ela continuará funcionando da mesma forma que antes. Desta forma, quando utilizar esta biblioteca, recomendo fazer uso desta classe BaseRepository. Ela não chega no nível da DbContext do Entity Framework, mas assim como ela, facilita o acesso ao banco.

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

O RepoDb ainda está crescendo, mas é uma clara boa alternativa quando não necessitar de algo robusto como o Entity e não queira algo muito simples como o Dapper. Portanto, recomendo que nestas situações dê uma chance para este framework, você notará as suas vantagens.

Ah, o código desta aplicação pode ser visto no meu Github.

Até a próxima.

O que é o ASP.NET Core?

O ASP.NET Core é uma versão do ASP.NET para a plataforma .NET Core. Só que antes de conhecê-lo precisamos voltar um pouco no tempo.

Em 2002, no lançamento da versão 1.0 do .NET Framework, a Microsoft lançou com esta plataforma o ASP.NET, como um sucessor da tecnologia ASP ( Active Server Pages).

Por se tratar de uma extensão do .NET Framework, assim como ele, no início o ASP.NET possuía poucos recursos. Suportava todas as linguagens de programação compatíveis com a plataforma .NET, mas ainda era muito limitado.

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

Replicando o desenvolvimento Desktop na web

No seu lançamento, o ASP.NET trouxe o modelo de aplicação Web Forms (ASP.NET Web Forms), cujo objetivo era replicar o desenvolvimento Desktop em uma aplicação web.

Em 2002, o desenvolvimento Desktop possuía ferramentas como Delphi e Visual Basic, que permitiam que uma tela fosse criada com o arrastar de controles. Desta forma, o desenvolvedor não precisava se preocupar com a codificação dos componentes da interface, apenas com a lógica de negócios.

Replicando isso na web, o ASP.NET Web Forms permitia que os desenvolvedores criassem interfaces web arrastando componentes. O ASP.NET se encarregaria de criar o HTML, CSS e JavaScript da página, e o desenvolvedor poderia focar na lógica de negócio da aplicação.

Olhando em retrospecto, na época esta foi uma boa solução, pois ajudou os desenvolvedores Desktop migrarem para o desenvolvimento web. Entretanto, o HTML gerado pelo ASP.NET tinha pouca legibilidade, hoje seria considerado um código muito poluído, por isso ao logo do tempo o Web Forms entrou em desuso e hoje está descontinuado.

Padrões de projetos vem ao resgate

Para melhorar a qualidade das aplicações ASP.NET, em 2009 foi lançado o modelo de aplicação MVC (ASP.NET MVC), cujo objetivo era aplicar o padrão de projetos MVC. Neste tipo de aplicação, os componentes são separados em três camadas lógicas:

  • Model (camada de negócio);
  • View (camada de apresentação);
  • Controller (camada de controle).

Um model representa o estado de um aspeto particular da aplicação. O controller lida com as interações e alterações do model para refletir o estado da aplicação e passa essas informações para a view. A view recebe as informações do controller e as exibe para o usuário.

Não possuindo mais o recurso de arrastar componentes para criar as páginas, mas isoladas na camada view, o ASP.NET MVC permitia que um desenvolvedor front-end pudesse focar na criação das páginas, enquanto outro lidasse com a lógica de negócio das camadas model e controller.

A constante busca pela evolução

Buscando uma evolução constante, ao longo do tempo foram lançados vários recursos ao ASP.NET, como: Web API, que permite criar APIs; Razor, uma template engine; ASP.NET AJAX, que facilita a adição de Ajax em aplicações ASP.NET; entre outros.

Nesta procura por evolução, a Microsoft notou que a comunidade poderia ajudá-la, assim, em 2012 decidiu abrir o código fonte do ASP.NET (entre outros produtos). E trabalhando em conjunto com a comunidade nas atualizações do ASP.NET, notou que a plataforma necessitava de muitas modificações que não poderiam ser aplicadas na versão existente, assim nasceu o ASP.NET Core.

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

Então surge o ASP.NET Core

Sucessor do ASP.NET, o ASP.NET Core é um framework open-source, multiplataforma, criado pela Microsoft e a comunidade. Leve, rápido e modular, funciona em conjunto com o .NET Core.

Lançado em 2016, mesmo sendo um sucessor do ASP.NET, o ASP.NET Core foi criado totalmente do zero, para que não precisasse se preocupar com código legado permitindo assim seguir o padrão de desenvolvimento web moderno.

Assim como outras plataformas, o ASP.NET Core é totalmente modular, recursos podem ser adicionados via pacotes Nuget. O que permitiu que a plataforma fosse mais performática que seu antecessor. Além disso, não necessita de uma IDE específica, todo desenvolvimento pode ser feito via linha de comando. O que permite que uma aplicação criada em uma plataforma possa ser movida para outra, sem a perda de nenhum recurso ou funcionalidade.

Devido a todo seu poder, caso necessite criar uma aplicação web, não deixe de dar uma olhada no ASP.NET Core.

Mediator Pattern com MediatR no ASP.NET Core

Para facilitar o desenvolvimento, a manutenção e manter o código limpo e legível, grandes aplicações procuram seguir princípios SOLID, padrões de projetos e outras recomendações de boas práticas, como a desacoplação dos objetos. Neste grupo de recomendações, vem ganhando espaço a adoção do Mediator Pattern, que no ASP.NET pode ser facilmente implementado com a biblioteca MediatR, como veremos neste artigo.

C# (C Sharp) - ASP.NET MVC
Curso de C# (C Sharp) - ASP.NET MVC
CONHEÇA O CURSO

O que é Mediator Pattern?

Mediator é um padrão de projetos comportamental especificado pela GoF, que na sua definição formal é descrito como:

Um objeto que encapsula a forma como um conjunto de objetos interage. O Mediator promove o acoplamento fraco ao evitar que os objetos se refiram uns aos outros explicitamente e permite variar suas interações independentemente.

Em outras palavras podemos dizer que o Mediator Pattern gerencia as interações de diferentes objetos. Através de uma classe mediadora, centraliza todas as interações entre os objetos, visando diminuir o acoplamento e a dependência entre eles. Desta forma, neste padrão, os objetos não conversam diretamente entre eles, toda comunicação precisa passar pela classe mediadora.

Podemos ilustrar seu funcionamento com a imagem abaixo:

Representação Gráfica do Mediator Pattern. Há setas apontando e saindo do objeto Mediator para representar a comunicação.

Se um objeto, por exemplo, Objeto A, quiser se comunicar com outro, Objeto C, terá que passar pelo Mediator. Com isso, cada objeto pode focar apenas na sua responsabilidade e não precisa conhecer a estrutura do outro para realizar a comunicação. Se Objeto C for modificado, Objeto A não precisa tomar conhecimento disso, ou seja, cada objeto trabalha de forma independente e isolada.

Entretanto, se houver um grande fluxo de comunicação entre os objetos, o objeto mediador pode se tornar um gargalo para a aplicação. Para resolver isso, a solução comum é implementar o CQRS (Command Query Responsibility Segregation), em tradução livre, Segregação de Responsabilidade de Comando e Consulta.

CQRS vem ao resgate

O Command Query Responsibility Segregation, ou CQRS, é um padrão de projeto que separa as operações de leitura e escrita da base de dados em dois modelos: queries e commands. Os commands são responsáveis pelas ações que realizam alguma alteração na base de dados. Geralmente operações assíncronas que não retornam nenhum dado.

Já as queries são responsáveis pelas consultas, retornando objetos DTOs (Data Transfer Object) para que seja isolada do domínio.

Por gerarem o maior fluxo de informações da aplicação, as consultas podem ser otimizadas através de um serviço específico, como um servidor de cache. Do lado dos commands, o Mediator pode ser utilizado para gerenciar a comunicação dos objetos.

É por causa disso que a biblioteca MediatR implementa conceitos do CQRS, porém não vou me aprofundar nele, já que não é objetivo desse artigo.

O MediatR entra em cena

O MediatR é uma biblioteca open source criada por Jimmy Bogard, o mesmo criador da biblioteca AutoMapper. O seu objetivo é facilitar a implementação do Mediator Pattern em aplicações .NET. Com ela não precisamos nos preocupar com a criação da classe mediadora, pois já são fornecidas interfaces que facilitam a implementação do fluxo de comunicação entre os objetos.

Colocando a mão na massa

Agora que conhecemos os conceitos por trás da biblioteca MediatR, vamos para um exemplo de implementação em um projeto. Será o projeto de uma API simples com um CRUD de pessoas.

A primeira coisa a ser feita é a criação do projeto:

dotnet new webapi -n MediatRSample

Em seguida, iremos adicionar a biblioteca:

dotnet add package MediatR

Para uma aplicação ASP.NET Core, também é necessário instalar o pacote de injeção de dependência:

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Com o projeto criado e as dependências adicionadas, vamos adicionar algumas pastas nele, para que o nosso código fique mais organizado. O projeto ficará com a seguinte estrutura:

Onde dentro da pasta Application, temos as pastas:

  • Commands: Onde serão definidos os objetos DTOs que representam uma ação a ser executada;
  • EventHandlers: Onde serão definidos objetos responsáveis por receber uma notificação gerada pelos Handlers;
  • Handlers: Onde serão definidos objetos responsáveis por receber as ações definidas pelos Commands;
  • Models: Onde serão definidas as entidades utilizadas pela aplicação;
  • Notifications: Onde serão definidos os objetos DTOs que representam notificações.

    Vamos implementar as classes dessas pastas.

C# (C Sharp) - ASP.NET MVC
Curso de C# (C Sharp) - ASP.NET MVC
CONHEÇA O CURSO

Especificando a classe domínio e o repositório da aplicação

Antes de implementar os objetos que farão uso da biblioteca MediatR, é necessário especificar a nossa classe de domínio, o model Pessoa:

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

Esta classe contém todas as propriedades que necessitamos para representar um objeto pessoa na nossa aplicação. Os commands que iremos definir serão baseados nela.

Por se tratar de um exemplo simples, esta aplicação não fará uso de um banco de dados, mas possuirá uma interface IRepository:

public interface IRepository<T>
{
    Task<IEnumerable<T>> GetAll();

    Task<T> Get(int id);

    Task Add(T item);

    Task Edit(T item);

    Task Delete(int id);
}

E uma classe repositório que salva os dados em uma coleção estática:

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

    public async Task<IEnumerable<Pessoa>> GetAll(){
        return await Task.Run(() => pessoas.Values.ToList());
    }

    public async Task<Pessoa> Get(int id){
        return await Task.Run(() => pessoas.GetValueOrDefault(id));
    }

    public async Task Add(Pessoa pessoa){
        await Task.Run(() => pessoas.Add(pessoa.Id, pessoa));
    }

    public async Task Edit(Pessoa pessoa){
        await Task.Run(() =>
        {
            pessoas.Remove(pessoa.Id);
            pessoas.Add(pessoa.Id, pessoa);
        });
    }

    public async Task Delete(int id){
        await Task.Run(() => pessoas.Remove(id));
    }
}

Implementando o padrão Command

Para gerenciar as interações dos objetos, a biblioteca MediatR implementa o padrão Command. Este padrão específica um objeto que encapsula toda informação necessária para executar uma ação posterior.

É neste ponto que a biblioteca faz uso do CQRS, como explicado anteriormente. Como vimos, no CQRS as operações são separadas em queries e commands. A parte dos commands é uma aplicação do padrão Command, que na implementação do MediatR é composta de dois objetos: Command e Command Handler.

Os objetos Command definem solicitações que irão alterar o estado dos dados e que o sistema precisa realizar. Por ser imperativo e se tratar de uma ação que será executada apenas uma vez (por solicitação) é recomendado que esses objetos sejam nomeados com o verbo no imperativo: “cadastra”, “altera”, etc. Também se recomenda a adição de um tipo, e.g., CadastraPessoaCommand, AlteraPessoaCommand, etc.

Já os objetos Command Handler serão responsáveis por executar as ações definidas pelos objetos Command. É aqui que ficará centralizado grande parte da lógica da aplicação.

Vamos iniciar a nossa implementação deste padrão definindo as classes Command:

public class CadastraPessoaCommand : IRequest<string>
{
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class AlteraPessoaCommand : IRequest<string>
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class ExcluiPessoaCommand : IRequest<string>
{
    public int Id { get; set; }
}

Note que essas são classes POCO simples, que contém apenas os atributos necessários para executar a ação especificada. Todas implementam a interface IRequest da biblioteca MediatR.

Nesta interface genérica se especifica o tipo de dado que será retornado quando o command for processado. Também é atráves do uso desta interface que será possível vincular os commands com as classes Command Handlers. É desta forma que a biblioteca saberá qual objeto deve ser invocado quando uma solicitação for gerada.

As boas práticas recomendam que para cada objeto Command haja um objeto Command Handler, entretanto seria possível implementar um objeto Command Handler para lidar com todos os commands definidos na aplicação.

Para este exemplo vou seguir as boas práticas, então a aplicação conterá as três classes abaixo:

public class CadastraPessoaCommandHandler : IRequestHandler<CadastraPessoaCommand, string>
{
    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;
    public CadastraPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    public async Task<string> Handle(CadastraPessoaCommand request, CancellationToken cancellationToken)
    {
        var pessoa = new Pessoa { Nome = request.Nome, Idade = request.Idade, Sexo = request.Sexo };

        try {
            pessoa = await _repository.Add(pessoa);

            await _mediator.Publish(new PessoaCriadaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo});

            return await Task.FromResult("Pessoa criada com sucesso");
        } catch(Exception ex) {
            await _mediator.Publish(new PessoaCriadaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo });
            await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
            return await Task.FromResult("Ocorreu um erro no momento da criação");
        }

    }
}
public class AlteraPessoaCommandHandler : IRequestHandler<AlteraPessoaCommand, string>
{
    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;
    public AlteraPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    public async Task<string> Handle(AlteraPessoaCommand request, CancellationToken cancellationToken)
    {
        var pessoa = new Pessoa { Id = request.Id, Nome = request.Nome, Idade = request.Idade, Sexo = request.Sexo };

        try {
            await _repository.Edit(pessoa);

            await _mediator.Publish(new PessoaAlteradaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo, IsEfetivado = true});

            return await Task.FromResult("Pessoa alterada com sucesso");
        } catch(Exception ex) {
            await _mediator.Publish(new PessoaAlteradaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo, IsEfetivado = false});
            await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
            return await Task.FromResult("Ocorreu um erro no momento da alteração");
        }

    }
}
namespace MediatRSample.Application.Handlers
{
    public class ExcluiPessoaCommandHandler : IRequestHandler<ExcluiPessoaCommand, string>
    {
        private readonly IMediator _mediator;
        private readonly IRepository<Pessoa> _repository;
        public ExcluiPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
        {
            this._mediator = mediator;
            this._repository = repository;
        }

        public async Task<string> Handle(ExcluiPessoaCommand request, CancellationToken cancellationToken)
        {
            try {
                await _repository.Delete(request.Id);

                await _mediator.Publish(new PessoaExcluidaNotification { Id = request.Id, IsEfetivado = true});

                return await Task.FromResult("Pessoa excluída com sucesso");
            } catch(Exception ex) {
                await _mediator.Publish(new PessoaExcluidaNotification { Id = request.Id, IsEfetivado = false });
                await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
                return await Task.FromResult("Ocorreu um erro no momento da exclusão");
            }

        }
    }
}

Note que todos os command handlers implementam a interface IRequestHandler, nesta interface é especificado uma classe command e o tipo de retorno. Quando esta classe command gerar uma solicitação, o MediatR irá invocar o command handler, chamando o método Handler.

É no método Handler que definimos as instruções que devem ser realizadas para aplicar a solicitação definida pelo command.

Após a solicitação ser atendida, é possível utilizar o método Publish() para emitir uma notificação para todo sistema. Onde o MediatR irá procurar por uma a classe que tenha implementado a interface INotificationHandler<tipo da notificacao> e invocar o método Handler() para processar aquela notificação, como veremos a seguir.

Implementando notificações

Na sua essência as solicitações command não retornam nenhuma informação, assim para ser informado que a solicitação foi finalizada com sucesso, ou não, pode ser implementada notificações.

Como vimos no tópico anterior, no método Handler de uma classe Command Handler, pode ser invocado o método Publish(), passando por parâmetro um objeto notificação. Todos os Event Handlers que estiverem “ouvindo” notificações do tipo do objeto “publicado” serão notificados e poderão processá-lo.

Assim, para implementar as notificações inicialmente é necessário definir os objetos notificação:

public class PessoaCriadaNotification : INotification
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class PessoaAlteradaNotification : INotification
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
    public bool IsEfetivado { get; set; }
}
public class PessoaExcluidaNotification : INotification
{
    public int Id { get; set; }
    public bool IsEfetivado { get; set; }
}
public class ErroNotification : INotification
{
    public string Excecao { get; set; }
    public string PilhaErro { get; set; }
}

Como as classes Commands, as notificações são classes POCO que contêm apenas os dados necessários para processar a informação. Para que a biblioteca reconheça o objeto dessas classes como uma notificação é importante que elas implementem a interface INotification.

Já a nossa classe Notification Handler irá “escutar” todas as notificações, pois todas serão apenas registradas no console:

public class LogEventHandler :
                            INotificationHandler<PessoaCriadaNotification>,
                            INotificationHandler<PessoaAlteradaNotification>,
                            INotificationHandler<PessoaExcluidaNotification>,
                            INotificationHandler<ErroNotification>
{
    public Task Handle(PessoaCriadaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"CRIACAO: '{notification.Id} - {notification.Nome} - {notification.Idade} - {notification.Sexo}'");
        });
    }

    public Task Handle(PessoaAlteradaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"ALTERACAO: '{notification.Id} - {notification.Nome} - {notification.Idade} - {notification.Sexo} - {notification.IsEfetivado}'");
        });
    }

    public Task Handle(PessoaExcluidaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"EXCLUSAO: '{notification.Id} - {notification.IsEfetivado}'");
        });
    }

    public Task Handle(ErroNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"ERRO: '{notification.Excecao} \n {notification.PilhaErro}'");
        });
    }
}

A aplicação poderia definir quantas classes Notification Handlers forem necessárias. Por exemplo, além da classe acima poderia haver uma classe que enviaria um e-mail para o usuário informando que uma pessoa foi cadastrada. Outra que avisaria a equipe de suporte que um erro foi gerado.

Caso uma notificação seja “ouvida” por mais de um Notification Handlers, todos serão invocados quando a notificação for gerada.

Controller e configuração MediatR

Agora que os objetos do MediatR estão criados, podemos definir o nosso controller:

[ApiController]
[Route("[controller]")]
public class PessoaController : ControllerBase
{

    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;

    public PessoaController(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok(await _repository.GetAll());
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        return Ok(await _repository.Get(id));
    }

    [HttpPost]
    public async Task<IActionResult> Post(CadastraPessoaCommand command)
    {
        var response = await _mediator.Send(command);
        return Ok(response);
    }

    [HttpPut]
    public async Task<IActionResult> Put(AlteraPessoaCommand command)
    {
        var response = await _mediator.Send(command);
        return Ok(response);
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        var obj = new ExcluiPessoaCommand { Id = id };
        var result = await _mediator.Send(obj);
        return Ok(result);
    }
}

Note que neste controller estamos “injetando” a interface IMediator, isso foi feito para que seja possível enviar as solicitações dos nossos objetos command com o método Send que esta interface disponibiliza:

var response = await _mediator.Send(command);

Neste contexto, o IMediator será a classe mediadora que através do método Send chama os command handlers que definimos, com base no objeto passado.

Por fim, é necessário adicionar o MediatR como serviço da nossa aplicação no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMediatR(typeof(Startup));
    services.AddSingleton<IRepository<Pessoa>, PessoaRepository>();
}

Também repare que estamos registrando a nossa classe repositório para que ela seja “injetada” no nosso controller e nas classes Command Handlers.

E a mágica acontece

Com a nossa aplicação definida, podemos iniciá-la e testar os endpoints. Note que ao executar uma solicitação:

Tela no Postman, mostrando o exemplo de uma requisição POST para o endpoint "https://localhost:5001/pessoa"

Ela será registrada no console:

Print do terminal do Visual Studio Code mostrando o texto: "CRIACAO: '1 - Carlos - 21 - M'"

Todas as solicitações que realizam alguma alteração nos dados serão registradas:

Print do terminal do Visual Studio Code mostrando logs do endpoint

Você pode ver o código completo da aplicação no repositório dela no meu Github.

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

No episódio de hoje aprendemos…

Mesmo com o exemplo simples demonstrado neste artigo, é possível notar que o Mediator Pattern nos ajuda a manter os objetos do sistema totalmente isolados e independentes, cada um possuindo apenas a sua responsabilidade.

Com a biblioteca MediatR a implementação deste padrão em uma aplicação ASP.NET Core é facilitada. Mas é importante frisar que este o Mediator não deve ser utilizado em qualquer projeto, dependendo do caso, a “sobrecarga ” necessária para implementá-lo não compensará os benefícios que fornece.

Então como He-Man diria, utilize com sabedoria.

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

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

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

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

Criando uma aplicação Worker Service

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

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

dotnet new worker -n WorkerSample

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

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

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

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

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

Definindo a tarefa

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

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

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

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

Publicando o serviço

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

dotnet publish -c Release

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

dotnet publish -c Release -r <RID>

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

Instalando o serviço no Windows

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

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

Instalando o serviço no Mac OS X

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

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

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

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

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

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

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

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

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

Instalando o daemon no Linux

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

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

[Unit]
Description=Dotnet Core Demo service

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

[Install]  
WantedBy=multi-user.target  

Após isso, basta habilitar o daemon:

systemctl enable worker-sample.service 

Que ele poderá ser iniciado:

systemctl start dotnet-sample-service.service

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

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

O Worker Service é bom?

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

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

Criando um middleware customizado para ASP.NET Core

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

Definindo o middleware customizado diretamente no pipeline

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

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

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

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

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

Definindo uma classe “Middleware”

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

public class MyMiddleware
{
    private readonly RequestDelegate _next;

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

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

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

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

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

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

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

E utilizá-lo no Configure:

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

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

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

Definindo configurações para o Middleware

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

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

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

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

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

        service.Configure(options);
        return service;
    }

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

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

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

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

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

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

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

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

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

E indicá-las ConfigureServices:

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

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

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

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

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

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

E modificar o construtor do middleware:

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

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

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

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

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

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

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

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

Conclusão

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

Criando um API Gateway com ASP.NET Core e Ocelot

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

Como demostrado na imagem abaixo:

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

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

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

API Gateway

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

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

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

A imagem abaixo ilustra bem o funcionamento do API Gateway:

Ocelot

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

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

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

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

Criando microsserviços de exemplo

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

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

Com isso, conseguiremos diferenciar cada uma.

Criando a API Gateway

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

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

dotnet new web -n Gateway

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

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

dotnet add package Ocelot

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

public class Program
{
    //Código omitido

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

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

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

public class Startup
{
    private readonly IConfiguration _configuration;

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

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

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

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

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

services.AddOcelot(_configuration);

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

app.UseOcelot().Wait();

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

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

Configurando a API Gateway

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

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

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

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

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

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

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

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

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

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

Ou até pela URL:

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

Rodando

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

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

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

Concluindo

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

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

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

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.

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

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!

C# (C Sharp) - ASP.NET MVC
Curso de C# (C Sharp) - ASP.NET MVC
CONHEÇA O CURSO

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