ASP .NET Core

Implementando cache de memória no ASP.NET Core

A performance de um site pode ser um fator determinante para o seu sucesso ou fracasso. Mesmo com a velocidade das conexões crescendo a cada ano, também cresce o número de usuários que fazem uso apenas de dispositivos móveis, onde essas velocidades são significativamente menores. Então toda a aplicação web que deseja se destacar precisa fornecer um bom desempenho e uma das formas de obter isso é implementando cache de memória.

Noções básicas de cache

O cache pode melhorar significativamente a performance e a escalabilidade de uma aplicação, reduzindo o processo necessário para gerar conteúdo. Desta forma, ele funciona melhor com conteúdos que não são muito alterados e tem um custo alto de geração.

O cache cria uma cópia do conteúdo que pode ser retornada muito mais rápida que a fonte original, como demonstra o diagrama abaixo:

Demostra o fluxo do cache.

É importante que a aplicação seja criada de uma forma que não dependa apenas do cache.

Cache no ASP.NET Core

O ASP.NET Core fornece suporte à alguns tipos de cache. O mais simples é o cache de memória, onde o conteúdo é salvo na memória do servidor. Neste tipo de cache, caso a aplicação faça uso de mais de um servidor (for uma aplicação distribuída), é importante que o balanceamento de carga esteja definido para que as requisições subsequentes de um usuário sempre sejam encaminhadas para o mesmo servidor. Caso contrário, a aplicação não conseguirá aproveitar o cache de memória.

Outro tipo de cache suportado pelo ASP.NET Core é o cache distribuído. Geralmente gerenciado por uma aplicação externa, este tipo de cache pode ser compartilhado entre várias instâncias da aplicação. Tornando o tipo de cache ideal para aplicações distribuídas.

Em ambos os casos, o cache trabalha com o armazenamento de dados seguinte do padrão chave-valor.

Neste artigo focaremos apenas no cache de memória.

Implementando cache de memória no ASP.NET Core

No ASP.NET Core, o cache de memória é representado pela interface IMemoryCache, que já é “injetada” nas dependências da aplicação automaticamente pelo framework. Assim, podemos ter acesso a ele no construtor da classe:

public class ProductsController : Controller
{
    private readonly IMemoryCache _cache;

    public ProductsController(IMemoryCache cache)
    {
        _cache = cache;
    }
}  

Com acesso ao cache, pode ser utilizado o método TryGetValue para tentar obter uma informação:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    List<Product> products;
    if (!_cache.TryGetValue<List<Product>>(cacheKey, out products))
    {
        products = await _context.Products.ToListAsync();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(10));

        _cache.Set(cacheKey, products, cacheOptions);
    }
    return View(products);
}

Note que se a informação não constar no cache, ela é salva:

_cache.Set(cacheKey, products, cacheOptions);

Durante o salvamento de um dado, pode ser definida algumas opções, no caso acima é definido o “SlidingExpiration”:

var cacheOptions = new MemoryCacheEntryOptions()
           .SetSlidingExpiration(TimeSpan.FromSeconds(10));

Que indica o tempo que o cache pode ficar inativo, antes de ser removido. É importante prestar atenção neste ponto, pois caso o usuário acesse a aplicação dentro deste tempo, ele é renovado, o que pode fazer com que este item nunca seja removido do cache, mostrando para o usuário algo defasado.

Para este caso, uma alternativa é também definir o “AbsoluteExpiration”:

var cacheOptions = new MemoryCacheEntryOptions()
    .SetSlidingExpiration(TimeSpan.FromSeconds(10))
    .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

Que define o tempo máximo que um item pode ser mantido no cache.

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

Uma alternativa para o TryGetValue é o uso do GetOrCreate:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";

    var products = await _cache.GetOrCreateAsync<List<Product>>(cacheKey, async entry => {

        entry.SlidingExpiration = TimeSpan.FromSeconds(10);
        entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30);
        return await _context.Products.ToListAsync();
    });

    return View(products);
}

Que tentará obter o item do cache e caso não exista, será adicionado nele.

Por fim, ainda pode ser utilizado o método Get que apenas retorna o item do cache, ou nulo, se ele não for encontrado:

public IActionResult Index()
{
    var cacheKey = "Products";

    var products = _cache.Get<List<Product>>(cacheKey);

    return View(products);
}

Limite do cache de memória

O ASP.NET Core não controla o tamanho do cache de memória, mesmo se não houver espaço na memória, ele tentará salvar a informação nela. Desta forma, para evitar este tipo de situação, podemos definir um limite para este cache.

Como a implementação padrão de IMemoryCache não implementa isso, caso queira definir este limite, é necessário fornecer uma implementação:

public class CustomMemoryCache 
{
    public MemoryCache Cache { get; set; }
    public CustomMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 2048
        });
    }
}

Ela pode ser adicionada nas dependências da aplicação:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<CustomMemoryCache>();
}

E obtidas no construtor das classes:

public class ProductsController : Controller
{
    private readonly MemoryCache _cache;

    public ProductsController(CustomMemoryCache cache)
    {
        _cache = cache;
    }
}  

Entretanto, como o ASP.NET Core não faz o controle do tamanho do cache, ao definir um, sempre que um item for salvo nele, é necessário definir o tamanho deste item:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";

    var products = await _cache.GetOrCreateAsync<List<Product>>(cacheKey, async entry => {

        entry.SlidingExpiration = TimeSpan.FromSeconds(10);
        entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30);
        entry.Size = 10;
        return await _context.Products.ToListAsync();
    });

    return View(products);
}

Este tamanho é arbitrário e não define uma unidade de medida, pode representar o tamanho do item em bytes, o número de caracteres de uma string, a quantidade de itens de uma coleção, etc. Por isso é importante que a aplicação utilize apenas uma unidade de medida. Ao atingir o limite, o cache não salvará mais nenhuma informação.

Conclusão

A implementação de cache de memória no ASP.NET Core é um processo simples que pode fornecer muitos benefícios. Desta forma, caso a sua aplicação necessite exibir muitos dados, que sofram poucas alternações é altamente recomendado que ela implemente cache.

Você pode ver a aplicação apresentada neste artigo no meu Github!

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.

Utilizando o NHibernate em uma aplicação ASP.NET Core

Neste artigo falaremos sobre o NHibernate no ASP.NET Core.

Persistir informações é um requisito básico de quase todos os sistemas. Não importando o tamanho da aplicação, ela necessitará armazenar dados. Às vezes será necessário lidar com apenas um pequeno grupo de tabelas, em outros cenários será necessário trabalhar com um grande e complexo banco de dados.

Quanto mais complexo for o banco, mais robusta deve ser a camada de acesso da aplicação. Felizmente existem frameworks ORM que auxiliam neste processo. No .NET Core, duas soluções se destacam quando se trata de lidar com uma complexa base de dados: Entity Core e NHibernate.

Como já abordei o Entity Core aqui antes, hoje falarei do NHibernate.

NHibernate

O NHibernate é um porte para .NET do framework Hibernate do Java, que é uns dos mais antigos e respeitados ORMs. Assim como o framework que o originou, o NHibernate é um projeto open-source maduro e utilizado em uma infinidade de projetos, principalmente projetos corporativos.

Isso ocorre porque possui uma grande gama de recursos: suporte nativos a vários banco de dados, várias estratégias de geração de ID, cache de segundo nível, entre outras coisas. E por possuir muitos recursos, a sua configuração não é tão simples quando o Entity Core, mas fornece mais opções de customização.

Para conhecê-lo na prática, vamos a um exemplo de CRUD simples.

Criando a aplicação

Neste artigo criarei uma aplicação ASP.NET Core MVC:

dotnet new mvc -n AspNetCoreNHibernate

Nela, adicione o pacote do NHibernate:

dotnet add package NHibernate

Da biblioteca Fluent NHibernate:

dotnet add package FluentNHibernate

E o conector do SQLite (que será o banco deste exemplo):

dotnet add package System.Data.SQLite

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

Criando e mapeando a entidade

No NHibernate é necessário definir a entidade de domínio, uma classe POCO:

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

É importante que todas as propriedades desta entidade sejam virtual.

Para que o NHibernate reconheça a entidade, é necessário mapeá-la. O padrão da biblioteca é um mapeamento por arquivo XML, mas graças ao FluentNHibernate, isso pode ser feito via código:

public class ProductMap: ClassMapping<Product>
{
    public ProductMap()
    {
        Id(x => x.Id, x =>
        {
            x.Generator(Generators.Increment);
            x.Type(NHibernateUtil.Int64);
            x.Column("Id");
        });

        Property(b => b.Name, x =>
        {
            x.Length(520);
            x.Type(NHibernateUtil.String);
            x.NotNullable(true);
        });

        Property(b => b.Quantity, x =>
        {
            x.Type(NHibernateUtil.Int32);
            x.NotNullable(true);
        });

        Property(b => b.Price, x =>
        {
            x.Type(NHibernateUtil.Double);
            x.Scale(2);
            x.Precision(15);
            x.NotNullable(true);
        });

        Table("Products");
    }
}

Com a classe mapeada, podemos registrar o NHibernate nos serviços da nossa aplicação.

Configurando o serviço do NHibernate

Seguindo o padrão do ASP.NET Core, criarei uma extensão para o nosso serviço:

public static class NHibernateExtensions
{
    public static IServiceCollection AddNHibernate(
        this IServiceCollection services, 
        string connectionString)
    {
        var mapper = new ModelMapper();
        mapper.AddMappings(typeof(NHibernateExtensions).Assembly.ExportedTypes);
        HbmMapping entityMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();

        var configuration = new Configuration();
        configuration.DataBaseIntegration(c =>
        {
            c.Dialect<SQLiteDialect>();
            c.ConnectionString = connectionString;
            c.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
            c.SchemaAction = SchemaAutoAction.Update;
            c.LogFormattedSql = true;
            c.LogSqlInConsole = true;
        });
        configuration.AddMapping(entityMapping);

        var sessionFactory = configuration.BuildSessionFactory();

        services.AddSingleton(sessionFactory);
        services.AddScoped(factory => sessionFactory.OpenSession());

        return services;
    }
}

Note que é indicado que as classes de domínio estão mapeadas em classes da aplicação. Também definimos as configurações de conexão e por fim, é criado a sessão.

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

Esta sessão é adicionada no escopo, para que a conexão com o banco de dados não fique sempre “aberta”. O banco será acessado por um objeto sessão e ao adicioná-la no escopo, ele só será criado quando for utilizado.

Por fim é necessário chamar o método AddNHibernate em ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddNHibernate(Configuration.GetConnectionString("SQLiteConnection"));
    services.AddControllersWithViews();
}

Com essas configurações já podemos acessar o banco de dados. Mas para este projeto criarei e implementarei o padrão repository.

Criando o repositório da aplicação

O nosso repositório será composto de uma interface:

public interface IRepository<T>
{
    Task Add(T item);
    Task Remove(long id);
    Task Update(T item);
    Task<T> FindByID(long id);
    IEnumerable<T> FindAll();
}

E sua implementação:

public class ProductRepository : IRepository<Product>
{
    private ISession _session;
    public ProductRepository(ISession session) => _session = session;
    public async Task Add(Product item)
    {
        ITransaction transaction = null;
        try
        {
            transaction = _session.BeginTransaction();
            await _session.SaveAsync(item);
            await transaction.CommitAsync();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            await transaction?.RollbackAsync();
        }
        finally
        {
            transaction?.Dispose();
        }
    }

    public IEnumerable<Product> FindAll() => 
                    _session.Query<Product>().ToList();

    public async Task<Product> FindByID(long id) => 
                    await _session.GetAsync<Product>(id);

    public async Task Remove(long id)
    {
        ITransaction transaction = null;
        try
        {
            transaction = _session.BeginTransaction();
            var item = await _session.GetAsync<Product>(id);
            await _session.DeleteAsync(item);
            await transaction.CommitAsync();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            await transaction?.RollbackAsync();
        }
        finally
        {
            transaction?.Dispose();
        }
    }

    public async Task Update(Product item)
    {
        ITransaction transaction = null;
        try
        {
            transaction = _session.BeginTransaction();
            await _session.UpdateAsync(item);
            await transaction.CommitAsync();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            await transaction?.RollbackAsync();
        }
        finally
        {
            transaction?.Dispose();
        }
    }
}

Note que nas operações que realizam alterações no banco, é criada uma transação:

transaction = _session.BeginTransaction();
await _session.SaveAsync(item);
await transaction.CommitAsync();

Isso não é necessário, mas é recomendado que seja implementado. Assim, caso ocorra um erro durante a alteração, é possível voltar o banco para o estado anterior a transação:

await transaction?.RollbackAsync();

Para finalizarmos, é necessário criar o controller.

Criando controller da aplicação

O nosso controller será um controller CRUD padrão:

public class ProductController : Controller
{
    private readonly ProductRepository productRepository;

    public ProductController(NHibernate.ISession session) => 
                        productRepository = new ProductRepository(session);

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

    // GET: Products/Details/5
    public async Task<ActionResult> Details(long? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        Product product = await 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 async Task<ActionResult> Create(
                    [Bind("Id,Name,Quantity,Price")]
                    Product product)
    {
        if (ModelState.IsValid)
        {
            await productRepository.Add(product);
            return RedirectToAction("Index");
        }

        return View(product);
    }

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

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

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

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

Antes de ver o sistema funcionando é necessário criar as views para as actions acima. Você pode vê-la no repositório do projeto.

Com isso, ao executá-lo, veremos que está tudo certo:

Página index mostrando a listagem de produtos

Conclusão

O NHibernate é uma ótima solução para aplicações que precisam lidar com bancos de dados complexos ou que não sejam suportado pelo Entity Core. Com vários anos de estrada é um framework popular e estável, que pode ser utilizado em qualquer projeto sem medo.

Então quando for criar o seu projeto, não deixe de dar uma olhada neste framework.

Você pode ver o código completo do projeto no meu Github.

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.

Amazon Web Services (AWS) - RDS - Fundamentos
Curso de Amazon Web Services (AWS) - RDS - Fundamentos
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) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
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) Intermediário
Curso de C# (C Sharp) Intermediário
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) - TDD
Curso de C# (C Sharp) - TDD
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.

Publicando uma aplicação ASP.NET Core no Heroku

No desenvolvimento de uma aplicação web pode ser necessário a sua publicação para testes, ou mesmo, para apresentar algo para um futuro usuário. Durante os estudos, também é muito comum o desejo de publicar a aplicação web que está sendo criada.

Por não ser algo definitivo, o desenvolvedor pode não desejar (ou não poder) pagar por uma hospedagem para publicar a aplicação web que está desenvolvendo. Infelizmente não existem muitas opções de hospedagem gratuita. Dos que existem, sempre há alguma limitação.

Um dos serviços que mais se destacam neste requisito é o Heroku.

Digital Ocean - Gerenciamento de Servidores e Serviços
Curso de Digital Ocean - Gerenciamento de Servidores e Serviços
CONHEÇA O CURSO

Heroku

O Heroku é uma platform as a service (PaaS) que permite que desenvolvedores criem, executem e publiquem aplicações na nuvem, sendo que a sua versão mais básica é grátis. Mesmo nesta versão, ele fornece uma URL, o que permite testar a aplicação sem necessitar de um domínio.

Entretanto, nativamente o Heroku fornece suporte apenas as linguagens JavaScript (via Node), Ruby, Java, PHP, Python, Go, Scala e Clojure. É possível executar outras linguagens via Buildpacks, que são scripts criados pela comunidade.

Contudo, esses buildpacks podem possuir limitações que impedem o seu uso na sua aplicação. Algo simples, para uma linguagem nativa, como a publicação de uma aplicação Django, pode ser complexo com o uso de um buildpack criado pela comunidade.

Felizmente, o Heroku também permite a publicação de uma imagem Docker, o que elimina praticamente todos os limites que um buildpack possa ter.

Assim, por não fornecer suporte nativo ao ASP.NET Core, neste artigo veremos como publicar este tipo de aplicação na plataforma via container do Docker.

Preparando a casa

Para acompanhar este artigo é importante que tenha o Docker instalado na sua máquina. Há versões dele para todos os sistemas operacionais. Também é importante que tenha uma conta no Heroku e o seu utilitário de linha comando configurado na máquina. O Fagner (instrutor aqui da Treinaweb) explicou como isso pode ser feito no seu artigo sobre a publicação de uma aplicação Django.

Por fim, mas não menos importante, a aplicação utilizada neste artigo será a API a criada com o framework Carter que mostrei anteriormente.

PHP - Testes unitários com PHPUnit
Curso de PHP - Testes unitários com PHPUnit
CONHEÇA O CURSO

Containeralizando” a aplicação ASP.NET Core

A primeira coisa que temos que fazer na aplicação é adicioná-la a um container Docker, para isso, deve ser adicionado ao projeto o arquivo Dockerfile. Será neste arquivo que definiremos como o nosso container será criado.

Também é recomendado que seja criado na aplicação um arquivo .dockerignore. Assim como o .gitignore, neste arquivo são definidos diretórios e arquivos que o docker deve ignorar. Para esta aplicação, o conteúdo do .dockerignore será:

**/.dockerignore
**/.git
**/.gitignore
**/.vs
**/.vscode
**/*.*proj.user
**/bin
**/obj

Já o Dockerfile conterá:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src
COPY . .
RUN dotnet restore 
RUN dotnet build --no-restore -c Release -o /app

FROM build AS publish
RUN dotnet publish --no-restore -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
# Padrão de container ASP.NET
# ENTRYPOINT ["dotnet", "CarterAPI.dll"]
# Opção utilizada pelo Heroku
CMD ASPNETCORE_URLS=http://*:$PORT dotnet CarterAPI.dll

Um arquivo Dockerfile é como um script. Este em questão implementa multi-stage, o que significa que é definido um passo para cada processo definido no script. Assim, se um passo falhar, não é necessário refazer os passos que já foram concluídos.

Acima, inicialmente se obtém a imagem de runtime do ASP.NET Core, na imagem do SDK as bibliotecas da aplicação são obtidas e ela é compilada. Caso não ocorra nenhum erro, a aplicação é publicada e por fim, ela é iniciada.

Geralmente quando uma aplicação ASP.NET Core é adicionada em um container, ela é iniciada com o comando:

ENTRYPOINT ["dotnet", "CarterAPI.dll"]

Mas o Heroku recomenda que a aplicação seja iniciada com o comando:

CMD ASPNETCORE_URLS=http://*:$PORT dotnet CarterAPI.dll

Isso ocorre porque o processo web do Heroku “escuta” as requisições HTTP na porta $PORT, que não necessariamente é a porta 80. A porta que ele utilizará é atribuído a variável de ambiente $PORT, assim antes de iniciar a aplicação, a porta utilizada pela aplicação é alterada para ser a mesma esperada pelo Heroku.

Agora está tudo pronto e podemos criar uma imagem da nossa aplicação. Para isso, utilizamos o comando abaixo:

docker build -t carter-api .

Lembrando que este comando deve ser executado pelo terminal no mesmo nível do arquivo Dockerfile, ou seja, na raiz da aplicação.

Pronto, com a imagem do projeto criado, podemos publicá-la no Heroku.

Publicando a aplicação no Heroku

Após se registrar no site, no dashboard crie um novo projeto:

O nome do projeto irá definir a URL da aplicação, que seguirá o padrão <nome-aplicacao>.herokuapp.com.

Voltando a aplicação, acesse pelo terminal a pasta da sua aplicação e nela execute o comando:

heroku container:push web -a carter-api

Note que no parâmetro -a é informado o nome da aplicação definida no Heroku. Com isso, uma imagem Docker será gerada e enviada para o registro do Heroku:

Como é apresentado no final deste comando, para que a aplicação seja disponibilizada é necessário executar o comando container:release:

heroku container:release web -a carter-api

Que irá apenas “liberar” a aplicação:

Agora é possível acessá-la na url <nome-aplicacao>.herokuapp.com:

Até o Swagger está disponível:

Simples não é?

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

Para tudo e além…

O processo que utilizamos aqui é bem simples e por fazer uso de um container Docker, isso nos permite publicar qualquer tipo de aplicação web que funciona em container. Isso vale desde aplicações de linguagens suportadas nativamente pelo Heroku, mas que utilize outra versão da linguagem, até aplicações que ainda nem existem atualmente.

Uma aplicação web de uma nova linguagem que suporte Docker, poderia ser publicada utilizando este mesmo processo, sem problemas.

Então é isso. Você pode obter os códigos da aplicação demostrada aqui no meu Github.

Criando um Chat com ASP.NET Core SignalR

No artigo passado, abordei aplicações em tempo real com a biblioteca SignalR, onde demonstrei seu uso em uma streaming API.

Como citei no artigo, um uso muito comum desta biblioteca é na criação de chats. Para conhecer mais recursos dela, vamos criar este tipo de aplicação.

Começando pelo começo

Antes de mais nada, criaremos uma aplicação web:

dotnet new web -n ChatSignalR

E nela, iremos criar um Hub:

namespace ChatSignalR.Hubs
{
    public class ChatHub: Hub
    {
        public async Task SendMessage(string usuario, string mensagem)
        {
            await Clients.All.SendAsync("ReceiveMessage", usuario, mensagem);
        }
    }
}

Note que para esta aplicação o Hub possui um método. Será para este método que o cliente enviará mensagens. Assim que a mensagem for recebida, o Hub a enviará para todos os clientes.

Esta será a dinâmica do nosso chat. Um chat aberto, onde todos os usuários conectados receberão todas as mensagens dos demais usuários.

Na parte do servidor, por fim, é necessário habilitar o SignalR e registrar o hub na classe Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services) => services.AddSignalR();

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

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ChatHub>("/chat");
        });
    }
}

Criando o cliente:

O nosso cliente será uma página web simples, que conterá um campo para o usuário informar seu nome e uma mensagem:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Mensagens</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" >
</head>
<body>
    <div class="container col-6">
        <div class="form-group">
            <label for="usuario">Usuário</label>
            <input type="text" id="usuario" class="form-control"/>
        </div>
        <div class="form-group">
            <label for="mensagem">Mensagem</label>
            <textarea class="form-control" id="mensagem" rows="2"></textarea>
        </div>
        <input type="button" class="btn btn-primary" id="send" value="Enviar Mensagem" />
    </div>
    <div class="row">
        <div class="col-12">
            <hr />
        </div>
    </div>
    <div class="container col-6">
        <ul class="list-group" id="messagesList"></ul>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/aspnet-signalr/1.1.4/signalr.min.js'></script>
    <script src='main.js'></script>
</body>
</html>

Na parte do JavaScript, conectaremos ao Hub do servidor e definiremos o envio e recebimento de mensagens:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chat").build();
$("#send").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = $("<li></li>").text(user + ": " + msg);
    li.addClass("list-group-item");
    $("#messagesList").append(li);
});

connection.start().then(function () {
    $("#send").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

$("#send").on("click", function (event) {
    var user = $("#usuario").val();
    var message = $("#mensagem").val();
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});

Note que quando uma mensagem for recebida, ela é exibida em uma lista:

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = $("<li></li>").text(user + ": " + msg);
    li.addClass("list-group-item");
    $("#messagesList").append(li);
});

E no envio é indicado o método definido no Hub:

$("#send").on("click", function (event) {
    var user = $("#usuario").val();
    var message = $("#mensagem").val();
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
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

1,2, 3…. Testando

Agora, ao executar a aplicação e abrir várias abas, note que todos os usuários irão receber as mensagens que forem enviadas:

Simples, não é?

Você pode obter o código da aplicação demonstrado neste artigo no meu Github.

Trabalhando com a template engine Liquid no ASP.NET Core

Quando pensamos em template engine para o ASP.NET, a primeira coisa que vem a mente é o Razor. É uma unanimidade o quanto esta template engine é poderosa e, graças ao suporte constante da Microsoft, sempre recebe novos recursos, como os Razor Componentes. Também tem o fato que ela está integrada ao ASP.NET Core, o que facilita o seu uso. Mas ela não é a única opção de template engine para o ASP.NET Core.

Neste quesito se destaca a biblioteca Fluid, que adiciona ao ASP.NET Core suporte a template engine Liquid.

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

Liquid

Liquid é uma template engine open source criada pela empresa Shopify para ser utilizado na criação de temas do seu CMS (sistema de gerenciamento de conteúdo). Possuindo uma sintaxe simples, ele facilita a definição de templates principalmente para quem não está habituado com programação.

Criada em 2006, com o tempo esta template engine passou a ser adotada por outras soluções, principalmente sistemas CMS, como o Orchard Core, que é um framework CMS para ASP.NET Core.

Criando a aplicação que utilizará o Liquid

Diferente do Razor, o Liquid pode ser implementado até em um projeto console (para fazer isso com o Razor é necessário utilizar alguma biblioteca de terceiro). Mas para este artigo, trabalharemos com um projeto web e veremos como substituir o Razor pelo Liquid.

Desta forma, inicialmente iremos criar uma aplicação web vazia:

dotnet new web -n SimpleWebLiquid

Em seguida, adicione no projeto a referência da biblioteca Fluid:

dotnet add package Fluid.MvcViewEngine

Por fim, habilite o Fluid no método ConfigureServices da classe Startup:

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

Como criamos uma aplicação web vazia, também é importante adicionar as rotas dos controllers no método Configure desta classe:

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

    app.UseStaticFiles();

    app.UseRouting();

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

Acima é definido como controller padrão Pessoa, pois o criaremos a seguir.

Definindo o modelo e controller do exemplo

Para exemplificar o uso do Liquid, será definido no projeto um model Pessoa:

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

Um repositório simples para ele:

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

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

    public void Add(Pessoa pessoa){
        pessoa.Id = pessoas.Count + 1;
        pessoas.Add(pessoa.Id, pessoa);
    }
}

E por fim, o controller:

public class PessoaController: Controller
{
    public readonly PessoaRepository repository;

    public PessoaController() => repository = new PessoaRepository();

    public IActionResult Index()
    {
        var pessoas = repository.GetAll();

        return View(pessoas);
    }

    public IActionResult Create() => View();

    [HttpPost]
    public IActionResult Create([FromForm] Pessoa pessoa)
    {
        repository.Add(pessoa);
        return RedirectToAction(nameof(Index));
    }
}

Note que este controller não diferente muito de um controller definido quando estamos trabalhando com o Razor. A diferença é em relação ao não uso do validador contra ataques XSRF/CSRF (mesmo sem o Razor, isso pode ser configurado na classe Startup, mas não faz parte do escopo deste artigo) e a definição da anotação [FromForm] no parâmetro da nossa action Create. Como não iremos trabalhar com modelos fortemente tipados nos templates, também não é possível validar os dados do modelo. Isso teria que ser feito “manualmente”.

Criando os templates Liquid

Assim como ocorre com o uso do Razor, o controller também irá procurar pelos templates Liquid das actions dentro de uma pasta com seu nome em Views. Assim, no projeto, crie esta estrutura de pastas:

+-- Views
|   +-- Pessoa

A biblioteca Fluid segue algumas conversões do Razor. Caso haja um arquivo chamado _ViewStart na pasta, ele será o primeiro arquivo carregado. Assim, dentro de Views crie um arquivo chamado _ViewStart.liquid e adicione o conteúdo abaixo:

{% layout '_Layout' %}

A tag {% layout [template] %} é utilizada para definir o template padrão utilizado nas views da pasta atual e todas as subpastas, funcionando como uma “master page”.

Acima, estamos indicando como template um arquivo chamado _Layout, assim o crie dentro da pasta atual (não se esqueça da extensão .liquid) e adicione o conteúdo abaixo:

<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{{ ViewData['Title'] }}</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-sm nnavbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container">
                    Exemplo de Liquid
                </div>
            </nav>
        </header>
        <div class="container">
            <main role="main" class="pb-3">
            {% renderbody %}
            </main>
        </div>  

        <footer class="border-top footer text-muted">
            <div class="container">
                © 2020 - Treinaweb
            </div>
        </footer>
        <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
    </body>
</html>

No código acima é importante frisar a tag {% renderbody %}. Ela indica o ponto onde o conteúdo das views será renderizado na template. Funcionando como o @RenderBody() do Razor.

Também note o trecho de exibição do título:

<title>{{ ViewData['Title'] }}</title>

Nele estamos utilizando duas chaves. Estas chaves são utilizadas para indicar objetos que devem ser renderizados. Desta forma, quando este template for processado o conteúdo de ViewData['Title'] será exibido no HTML gerado.

Com o nosso template definido, podemos criar dentro da pasta Pessoa, a view de listagem (Index.liquid):

<p>
    <a href="/Pessoa/Create">Adicionar pessoa</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                Id
            </th>
            <th>
                Nome
            </th>
        </tr>
    </thead>
    <tbody>
    {% for p in Model %}
        <tr>
            <td>
                {{ p.Id }}
            </td>
            <td>
                {{ p.Nome }}
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>

E de cadastrado (Create.liquid):

<div class="row">
    <div class="col-md-4">
        <form action="/Pessoa/Create" method="post">
            <div class="form-group">
                <label for="Nome" class="control-label"></label>
                <input name="Nome" id="Nome class="form-control" />
            </div>
            <div class="form-group">
                <input type="submit" value="Salvar" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a href="/Pessoa/Index">Voltar para listagem</a>
</div>

Na view Index, note que estamos utilizando um laço for:

{% for p in Model %}

Este laço percorre um objeto Model. Para que o template reconheça a estrutura deste model, é necessário que ele seja registrado no construtor da classe Startup:

static Startup() => TemplateContext.GlobalMemberAccessStrategy.Register<Pessoa>();

Pronto, agora podemos testar a nossa aplicação.

Windows Server 2016 - Internet Information Services
Curso de Windows Server 2016 - Internet Information Services
CONHEÇA O CURSO

A mágica acontecendo

Ao executar a aplicação e acessá-la, nos é exibido a tela de listagem:

Também podemos adicionar algumas pessoas:

E notaremos que o nosso template Liquid está funcionando corretamente:

liquid_3

Devo migrar todas as minhas aplicações para o Liquid?

O suporte do Liquid ao ASP.NET não significa que você deve utilizá-lo em todas as suas aplicações. Como dito no início, o Razor é uma poderosa ferramenta, que não deve ser substituída sem necessidade.

O Liquid é uma template engine mais acessível para pessoas com pouco conhecimento em programação. Então quando estiver trabalhando em um projeto onde as pessoas que irão lidar com as templates possuem esta característica, você deve dar uma olhada nesta template engine.

Você pode ver o código completo da aplicação demonstrada neste artigo no meu Github.

ASP.NET Core – Criando uma Streaming API

Quando falamos de streaming de dados, a primeira coisa que vem a cabeça são streaming mídia: áudio e mídia. Mas em algumas situações, pode ser necessário a implementação de streaming de dados textuais.

Utilizado para a entrega de dados em tempo real, o streaming de dados em uma API é uma técnica utilizada principalmente por redes sociais, como Twitter e Facebook.

No .NET Core este tipo de recurso pode ser implementado via SignalR, mas neste artigo exemplificaremos isso utilizando a estrutura tradicional do ASP.NET Core. Futuramente abordarei o uso do SignalR.

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

Criando uma Web API Simples

Para exemplificar a API Streaming, inicialmente iremos criar uma API simples:

dotnet new web -n ApiSimples

Que conterá apenas um model:

namespace ApiSimples.Models
{
    public class Item
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }

        public override string ToString() => $"{Id} - {Name} - {IsComplete}";
    }
}

E um controller:

namespace ApiSimples.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoController : ControllerBase
    {
        private static List<Item> _itens = new List<Item>();

        [HttpGet]
        public ActionResult<List<Item>> Get() => _itens;

        [HttpPost]
        public async Task<ActionResult<Item>> Post([FromBody] Item value)
        {
            if(value == null)
                return BadRequest();

            if(value.Id == 0)
            {
                var max = _itens.Max(i => i.Id);
                value.Id = max+1;
            }

            _itens.Add(value);
            return value;
        }

        [HttpPut("{id}")]
        public async Task<ActionResult<Item>> Put(long id, [FromBody] Item value)
        {
            var item = _itens.SingleOrDefault(i => i.Id == id);
            if(item != null)
            {
                _itens.Remove(item);
                value.Id = id;
                _itens.Add(value);
                return item;
            }

            return BadRequest();
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> Delete(long id)
        {
            var item = _itens.SingleOrDefault(i => i.Id == id);
            if(item != null)
            {
                _itens.Remove(item);
                return Ok(new { Description = "Item removed" });
            }

            return BadRequest();
        }
    }
}

À frente este controller será modificado com adição do streaming.

Não se esqueça de adicionar o suporte para o controller em Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services) => services.AddControllers();

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

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Adicionando o streaming de dados

No ASP.NET Core, ao acessar uma rota, a solicitação é processada e o seu resultado retornado para o usuário. Ao definir um streaming de dados, a solicitação se manterá em um processamento contínuo, retornando dados de um evento, enquanto a conexão se mantiver ativa.

Por não seguir o comportamento padrão, o streaming não pode retornar um objeto ActionResult, que é o tipo de retorno mais comum. Assim, a primeira coisa a ser feita é definir um ActionResult customizado:

public class StreamResult : IActionResult
{
    private readonly CancellationToken _requestAborted;
    private readonly Action<Stream, CancellationToken> _onStreaming;

    public StreamResult(Action<Stream, CancellationToken> onStreaming, CancellationToken requestAborted)
    {
        _requestAborted = requestAborted;
        _onStreaming = onStreaming;
    } 

    public Task ExecuteResultAsync(ActionContext context)
    {
        var stream = context.HttpContext.Response.Body;
        context.HttpContext.Response.GetTypedHeaders().ContentType = new MediaTypeHeaderValue("text/event-stream");
        _onStreaming(stream, _requestAborted);
        return Task.CompletedTask;
    }
}

Note que esta classe implementa a interface IActionResult. Assim, ela poderá ser utilizada no lugar de ActionResult.

O ponto mais importante dela é a definição do callback onStreaming, que é recebido por parâmetro:

public StreamResult(Action<Stream, CancellationToken> onStreaming, CancellationToken requestAborted)
{
  _requestAborted = requestAborted;
  _onStreaming = onStreaming;
} 

Este callback será utilizado para salvar os clients que estiverem “ouvindo” o streaming. E o CancellationToken será utilizado para finalizar o streaming caso a solicitação seja interrompida/cancelada.

Voltando ao controller, é necessário definir uma coleção para salvar os clients:

private static ConcurrentBag<StreamWriter> _clients = new ConcurrentBag<StreamWriter>();

Esta coleção precisa ser thread-safe, por isso que foi utilizada uma coleção do namespace System.Collections.Concurrent.

Agora é possível definir o endpoint do streaming de dados:

[HttpGet]
[Route("streaming")]
public IActionResult Streaming()
{
    return new StreamResult(
        (stream, cancelToken) => {
            var wait = cancelToken.WaitHandle;
            var client = new StreamWriter(stream);
            _clients.Add(client);

            wait.WaitOne();

            StreamWriter ignore;
            _clients.TryTake(out ignore);
        }, 
        HttpContext.RequestAborted);
}

Note que na função callback da classe StreamResult estamos adicionando o client na coleção e aguarda-se que a solicitação seja cancelada. Com isso, ao acessar o endpoint, a solicitação se manterá ativa.

No momento já temos um streaming de dados, mas nada está sendo enviado para os clients. Para isso, definiremos um método que irá percorrê-los e escreverá dados no stream:

private async Task WriteOnStream(Item data, string action)
{
    foreach (var client in _clients)
    {
        string jsonData = string.Format("{0}\n", JsonSerializer.Serialize(new { data, action }));
        await client.WriteAsync(jsonData);
        await client.FlushAsync();
    }
}

Os dados escritos, serão as ações do nosso controller:

public async Task<ActionResult<Item>> Post([FromBody] Item value)
{
    if(value == null)
        return BadRequest();

    if(value.Id == 0)
    {
        var max = _itens.Max(i => i.Id);
        value.Id = max+1;
    }

    _itens.Add(value);

    await WriteOnStream(value, "Item added");

    return value;
}

[HttpPut("{id}")]
public async Task<ActionResult<Item>> Put(long id, [FromBody] Item value)
{
    var item = _itens.SingleOrDefault(i => i.Id == id);
    if(item != null)
    {
        _itens.Remove(item);
        value.Id = id;
        _itens.Add(value);

        await WriteOnStream(value, "Item updated");

        return item;
    }

    return BadRequest();
}

[HttpDelete("{id}")]
public async Task<ActionResult> Delete(long id)
{
    var item = _itens.SingleOrDefault(i => i.Id == id);
    if(item != null)
    {
        _itens.Remove(item);
        await WriteOnStream(item, "Item removed");
        return Ok(new { Description = "Item removed" });
    }

    return BadRequest();
}

Pronto, o nosso streaming de dados já está completo.

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

123 Testando…

Para testar a API, inicialmente é necessário acessar o endpoint do streaming de dados:

Note que o navegador indicará que a página está sendo carregada. Este é o comportamento padrão. Ao enviar uma solicitação para o controller, ela será mostrada nesta página:

A página sempre mostrará que está sendo carregada, mas todas as ações que forem geradas do nosso controller serão exibidas nela.

Simples, não é? Você pode ver esta aplicação completa no meu Github.

No próximo artigo mostrarei como obter isso utilizando o SignalR.

© 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