.NET Core

ASP.NET Core: Compreendendo AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages()

Neste artigo veremos as diferenças entres os métodos AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages() e quando utilizar cada um.

Tratando-se de uma reescrita do ASP.NET, a sua versão Core sempre foi pensada como um framework modular. Isso significa que as aplicações podem (e devem) adicionar apenas os recursos que utilizarão, o que irá otimizar a sua execução.

Infelizmente geralmente isso não é seguido, uma prova é a pouca adoção do método AddMvcCore().

AddMvcCore

Caso seja um iniciante no ASP.NET Core, talvez nunca tenha ouvido falar no método AddMvcCore(), mesmo na documentação oficial não é muito citado. Ele é um método de extensão de IServiceCollection, que carrega apenas os recursos básicos de uma aplicação ASP.NET:

  • Controllers: reconhece os controllers da aplicação;
  • Rotas: adiciona as rotas padrão;
  • CoreServiecs: adiciona os recursos básicos do ASP.NET.

Por adicionar os recursos básicos, qualquer outro recurso precisa ser adicionado “na mão”.

Por exemplo, crie uma aplicação web vazia e nela adicione um controller qualquer, com um método para a solicitação POST:

[Route("api/[controller]")]
public class HomeController : Controller
{
    [HttpPost]
        public ActionResult Post([FromBody] Usuario user)
    {
        if(ModelState.IsValid)
            return Ok(user);
        else
            return BadRequest(ModelState);
    }
}

E um model com ao menos um campo obrigatório:

public class Usuario
{
    public int Id { get; set; }
    public string Nome { get; set; }
    [Required]
    public string User { get; set; }
    [Required]
    public string Senha { get; set; }
}

Caso no método ConfigureServices seja adicionado apenas o método AddMvcCore():

public void ConfigureServices(IServiceCollection services) => services.AddMvcCore().AddNewtonsoftJson();

Obs.: Estou usando o .NET Core 3.0 Preview 4 e nesta versão as classes do Json.NET foram removidas da biblioteca padrão, assim, é necessário adicioná-las com o pacote Microsoft.AspNetCore.Mvc.NewtonsoftJson e o método AddNewtonsoftJson() utilizado acima.

O ASP.NET não irá validar os campos obrigatórios na solicitação POST:

Para que isso ocorra é necessário adicionar as Data Annotations:

public void ConfigureServices(IServiceCollection services) => services.AddMvcCore().AddNewtonsoftJson().AddDataAnnotations();

Com isso, a validação funcionará:

Claro que este nosso exemplo é simples, mas imagine ter que especificar autorização (a anotação [Authorize] ), CORS, etc. Por causa disso, geralmente utiliza-se o método AddMvc().

AddMvc

Assim como o AddMvcCore(), o AddMvc() é um método de extensão de IServiceCollection que carrega praticamente todos os recursos necessários de uma aplicação ASP.NET, como:

  • MvcCore: as dependências mínimas para executar uma aplicação ASP.NET Core MVC. Reconhece os controllers, realiza os bindings, adiciona das rotas padrão e os recursos básicos do ASP.NET;
  • ApiExplorer: Habilita as API Help pages;
  • Authorization: Habilita autorização;
  • FormatterMappings: Traduz a extensão de um arquivo para content-type;
  • Views: Habilita as Views;
  • RazorViewEngine: Habilita o Razor nas Views (páginas salvas com a extensão cshtml);
  • RazorPages: Habilita as Razor Pages (um tipo de aplicação do ASP.NET Core);
  • TagHelper: Adiciona as tags helpers do Razor;
  • Data Annotations: Habilita validação dos model por data annotations;
  • Json Formatters: Adiciona parsers de JSON;
  • CORS: Habilita o Cross-origin resource sharing (CORS).

Por adicionar vários recursos geralmente opta-se por definir apenas o método AddMvc(), em detrimento do AddMvcCore().

Ao fazer esta mudança na nossa aplicação:

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

Não é necessário especificar o Data Annotations, para que a validação seja realizada:

Mas caso esteja desenvolvendo uma aplicação Web API, o método AddMvc() irá carregar recursos que não serão utilizados. Nesta situação, o ideal seria optar pelo AddMvcCore(), mas se notar, até a versão 2.2 do ASP.NET a aplicação Web API criada pelo template padrão, também faz uso do método AddMvc().

Para alterar este comportamento, na versão 3.0 (que no momento da publicação deste artigo está em preview 4) foram adicionados os métodos AddControllers(), AddControllersWithViews() e AddRazorPages().

AddControllers

Podemos dizer que o método AddControllers() é uma evolução do AddMvcCore(), já que foi criado visando preencher a lacuna que este método não conseguiu. Para isso, como o seu nome sugere, ele carrega recursos relacionados aos controllers:

  • Controllers
  • Model Binding;
  • ApiExplorer;
  • Authorization;
  • CORS;
  • Data Annotations;
  • FormatterMappings.

Desta forma, se a aplicação não possuir uma interface, como a do nosso exemplo, que é uma Web API, pode-se optar por ele:

public void ConfigureServices(IServiceCollection services) => services.AddControllers().AddNewtonsoftJson();

Que todos os recursos relacionados a este tipo de aplicação serão carregados.

AddControllersWithViews

O método AddMvc() não foi descontinuado na versão 3.0, mas foi adicionando o método AddControllersWithViews() que carrega praticamente os mesmos recursos:

  • MvcCore;
  • ApiExplorer;
  • Authorization;
  • FormatterMappings;
  • Views;
  • RazorViewEngine;
  • TagHelper;
  • Data Annotations;
  • Json Formatters;
  • CORS.

O único que foi removido é o Razor Pages. Desta forma, uma aplicação ASP.NET MVC que não faça uso do Razor Pages deve optar por este método.

Para exemplificá-lo, é necessário adicionar uma view na aplicação:

@{
    ViewData["Title"] = "Home Page";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"]</title>
</head>
<body>
    <p>Aplicativo de exemplo!</p>
</body>
</html>

Definir uma action no controller:

public IActionResult Index()
{
    return View();
}

E definir as rotas no método Configure:

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

Com isso, a view será mostrada:

AddRazorPages

Razor Pages é um tipo de aplicação que permite adicionar códigos C# e HTML em um mesmo arquivo ou em arquivos separados, de uma forma muito similar ao antigo Web Forms. Por ser um aspecto de uma aplicação ASP.NET MVC, o AddRazorPages() carrega quase os mesmos recursos do método AddControllersWithViews():

  • RazorPages;
  • MvcCore;
  • Authorization;
  • Views;
  • RazorViewEngine;
  • TagHelper;
  • Data Annotations;
  • Json Formatters.

Os recursos que não são carregados por padrão são:

  • ApiExplorer;
  • FormatterMappings;
  • CORS.

Como o nome sugere este método deve ser utilizado em aplicações Razor Pages.

Mas caso a sua aplicação faça uso do Razor Pages e do ASP.NET MVC padrão, ele pode ser utilizado em conjunto com AddControllers() ou AddControllersWithViews():

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddControllersWithViews();
}

Que irá gerar o mesmo resultado do uso do AddMvc().

Conclusão

Por ser um framework modular, o ideal é que a aplicação ASP.NET carregue apenas os recursos que necessita. Comparando os métodos AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages(); e vendo quais recursos cada um carrega, fica claro a função de cada um e quando utilizá-los de acordo com cada tipo de aplicação.

Assim, procure sempre utilizar o método que for mais adequado ao tipo de aplicação que está desenvolvendo, mesmo se o template padrão do ASP.NET sugerir outro método.

Implementando o protocolo tus em uma aplicação ASP.NET Core

Upload de arquivo é uma tarefa recorrente de qualquer aplicação web. Não importando a linguagem, é simples a implementação deste recurso na aplicação. Mas quando se trata de grandes arquivos, nos deparamos com algumas situações pouco agradáveis.

Por exemplo, você precisa upar um arquivo grande, de 10GB para mais. Espera por horas pelo seu upload. Quando está quase acabando, a conexão cai, por algo no servidor, falta de internet, energia, etc. O upload será abortado e você terá que iniciá-lo do zero. Só quem já passou por isso, sabe o quanto é frustrante.

Caso não tenha uma boa conexão, mesmo com arquivos pequenos, o upload pode ser frustrante. Já que geralmente neste tipo de situação, o usuário envia apenas o início do arquivo várias vezes.

Para tentar resolver este tipo de problema foi criado o protocolo tus.

O que é o protocolo tus?

tus é um protocolo aberto para “recuperação” de upload de arquivo em HTTP. Isso significa que o usuário não precisa reiniciar o upload sempre que ocorre um erro no processo ou mesmo enviar todo o arquivo em apenas uma conexão. Permitindo que o usuário pause o processo ou mesmo o envie em uma conexão instável.

Eles possuem uma comunidade bem ativa no Github e grandes empresas que o implementam, como o Cloudflare e o Vimeo.

Funcionamento do protocolo

Na documentação do protocolo é possível ver como ele funciona em detalhes, mas o processo é simples. O servidor recebe solicitações POST, sem conteúdo, que contenha no seu header, os atributos abaixo:

  • Upload-Length: O tamanho, em bytes, do arquivo que será enviado;
  • Upload-Metadata: Atributo opcional, que pode ser utilizado para enviar informações do arquivo para o servidor, como nome, extensão, etc. Essas informações devem ser organizadas em pares de chave-valor, separados por vírgula. A chave e valor devem ser separados por espaço. Assim, os pares de chave-valor não podem conter espaço ou vírgula;
  • Tus-Resumable: Versão do protocolo.

Ele irá responder, com o código 201 (Created) e com os atributos abaixo no header:

  • Location: URL que deve ser utilizada para o upload do arquivo;
  • Tus-Resumable: Versão do protocolo.

Com isso, o usuário poderá utilizar a URL retornada na solicitação POST para efetuar o upload do arquivo. Este upload deve ser feito em solicitações PATCH e que contenha os atributos abaixo:

  • Upload-Offset: Indica em qual ponto do arquivo (baseado em um array de bytes) o conteúdo enviado deve ser adicionado;
  • Content-Length: Tamanho do conteúdo enviado;
  • Content-Type: Tipo do conteúdo, que sempre deve ser application/offset+octet-stream;
  • Tus-Resumable: Versão do protocolo.

Essas solicitações são respondidas com o código 204 (No Content) e com os atributos abaixo no header:

  • Upload-Offset: Valor atual do upload-offset;
  • Tus-Resumable: Versão do protocolo.

O servidor também pode aceitar solicitações DELETE, que permite excluir um upload não finalizado e OPTIONS, que retorna informações do uso do protocolo, como versão (Tus-Version), recursos implementados (Tus-Extension) e tamanho máximo de arquivo aceito (Tus-Max-Size).

Implementando em uma aplicação ASP.NET Core

Pelo funcionamento, notamos que a sua implementação não é complexa, mas requer um pouco de trabalho. Felizmente há bibliotecas que facilitam a implementação deste protocolo em uma aplicação ASP.NET Core.

Para mostrá-lo na prática, vamos criar uma aplicação de exemplo:

dotnet new web -n ExemploTus

Na página do protocolo são listadas algumas implementações dele, tanto para o lado do client quanto para o server. No caso do .NET, a implementação é recomentada para o servidor é a do Stefan Matsson, o tusdotnet. Assim, a adicione no projeto:

dotnet add package tusdotnet

Para configurá-lo, na classe Startup, no método Configure adicione o código abaixo:

app.UseTus(context => new DefaultTusConfiguration
{
    //Local onde os arquivos temporários serão salvos
    Store = new TusDiskStore(tempPath),
    // URL onde os uploads devem ser realizados.
    UrlPath = "/upload",
    Events = new Events
    {
        //O que fazer quando o upload for finalizado
        OnFileCompleteAsync = async ctx =>
        {
            var file = await ((ITusReadableStore)ctx.Store).GetFileAsync(ctx.FileId, ctx.CancellationToken);
            await ProcessFile.SaveFileAsync(file, env);
        }
    }
});

A biblioteca possui vários eventos, na documentação dela você pode conhecer quais são gerados.

No nosso caso, estamos apenas definindo a URL que será utilizada para o upload, o local onde os arquivos temporários serão salvos e estamos salvando em outro ponto do disco o arquivo quando seu upload for finalizado.

Não se esqueça de alterar o tamanho limite de arquivos aceitos pela aplicação. Isso pode ser feito no método CreateWebHostBuilder da classe Program:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureKestrel(options =>
        {
            options.Limits.MaxRequestBodySize = null;
            options.Limits.MaxRequestBufferSize = null;
        });

Acima está sendo definido um limite “infinito”, mas você pode informar qualquer valor em bytes.

Para testar isso, vamos utilizar a biblioteca JS disponibilizada pelo tus, que pode ser baixada aqui.

Com isso, poderemos definir um arquivo HTML, onde o upload será testado:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Upload de arquivo</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body>
    <div class="jumbotron jumbotron-fluid">
        <div class="container">
            <h1 class="display-4">Upload de arquivo</h1>
            <p class="lead">
                Para testar o protocolo, selecione um arquivo e clique em "Upload". Aguarde um pouco cancele, feche o navegador ou atualizado a página e inicie novamente 🙂
            </p>
            <p>
                <a href="#" onclick="resetLocalCache(event)">Clique aqui para limpar o cache do navegador!</a>
            </p>

            <form>
                <div class="custom-file">
                    <input type="file" class="custom-file-input" id="fileUpload" onchange="alterarNome()">
                    <label class="custom-file-label" for="fileUpload" id="fileUploadLabel">Selecione o arquivo</label>
                </div>
                <div class="form-group mt-2 text-right">
                    <input type="button" id="uploadButton" value="Upload" onclick="uploadFile()" class="btn btn-primary mb-2" />
                    <input type="button" id="cancelUploadButton" value="Cancelar" onclick="cancelUpload()" class="btn btn-primary mb-2" disabled />
                </div>
            </form>
            <progress value="0" max="100" id="uploadProgress" class="w-100" style="display:none" ></progress>
            <span id="info"></span>
        </div>
    </div>
    <script src="tus.js"></script>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    <script>
        var uploadProgress = document.getElementById('uploadProgress');
        var info = document.getElementById('info');
        var cancelUploadButton = document.getElementById('cancelUploadButton');
        var uploadButton = document.getElementById('uploadButton');
        var fileUpload = document.getElementById('fileUpload');
        var fileUploadLabel = document.getElementById('fileUploadLabel');
        var upload;

        function uploadFile() {
            var file = fileUpload.files[0];

            uploadProgress.value = 0;
            uploadProgress.removeAttribute('data');
            uploadProgress.style.display = 'block';

            disableUpload();

            info.innerHTML = '';

            upload = new tus.Upload(file,
                {
                    endpoint: 'upload/',
                    onError: onTusError,
                    onProgress: onTusProgress,
                    onSuccess: onTusSuccess,
                    metadata: {
                        filename: file.name,
                        contentType: file.type || 'application/octet-stream'
                    }
                });

            setProgressTest('Upload iniciado...');
            upload.start();
        }

        function cancelUpload() {
            upload && upload.abort();
            setProgressTest('Upload abortado');
            uploadProgress.value = 0;
            enableUpload();
        }

        function resetLocalCache(e) {
            e.preventDefault();
            localStorage.clear();
            alert('Cache limpo');
        }

        function onTusError(error) {
            alert(error);
            enableUpload();
        }

        function onTusProgress(bytesUploaded, bytesTotal) {
            var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);

            uploadProgress.value = percentage;
            setProgressTest(bytesUploaded + '/' + bytesTotal + ' bytes uploado');
        }

        function onTusSuccess() {
            setProgressTest('Upload finalizado!');
            enableUpload();
        }

        function setProgressTest(text) {
            info.innerHTML = text;
        }

        function enableUpload() {
            uploadButton.removeAttribute('disabled');
            cancelUploadButton.setAttribute('disabled', 'disabled');
        }

        function disableUpload() {
            uploadButton.setAttribute('disabled', 'disabled');
            cancelUploadButton.removeAttribute('disabled');
        }

        function alterarNome(){
            var fileName = fileUpload.files[0].name;
            fileUploadLabel.innerHTML = fileName;
        }
    </script>

</body>
</html>

Ao executar a aplicação, poderemos testar o upload de um arquivo grande e ver o resultado:

Você pode ver o código desta aplicação de exemplo no meu Github, aqui.

Conclusão

Caso a sua aplicação necessite efetuar o upload de arquivos grandes, devido ao seu poder, adicione o suporte ao protocolo tus. Isso irá facilitar muito o envio de arquivos para os usuários.

E como vimos, é muito simples implementar este suporte em uma aplicação ASP.NET Core. Assim, não há desculpa para não utilizar este protocolo.

Criando um provider customizado para o Microsoft.Extensions.Logging

Caso já tenha desenvolvido uma aplicação ASP.NET Core, deve ter deparado com o seu sistema de log. Adicionado desde a primeira versão desta biblioteca, o ILoggingBuilder da Microsoft.Extensions.Logging é o responsável por registrar nos Services provedores de log para este tipo de aplicação.

Internamente, o provedor recebe do LoggerFactory todas as informações geradas durante a execução da aplicação, tanto em tempo de desenvolvimento quanto em produção.

Mesmo com uma série de provedores (nativos e de terceiros), em alguns casos é necessário criar um provedor customizado e neste artigo veremos como isso pode ser feito.

Anteriormente em um projeto ASP.NET Core…

Antes de vermos a criação do provedor, um resumo da biblioteca Microsoft.Extensions.Logging.

Esta biblioteca foi criada como uma API de log, que os desenvolvedores podem utilizar para capturar os logs internos do ASP.NET Core, bem como gerar logs customizados. Esses logs são enviados para provedores, que ficam responsáveis de registrar as mensagens.

No momento, há os seguintes provedores nativos:

E também temos os seguintes provedores de bibliotecas de terceiro:

Caso nenhuma dessas bibliotecas atendam as suas necessidades, chega a hora de criar um provedor customizado, é o que faremos a seguir.

Primeiro Ato – Preparando o ambiente

Neste exemplo, iremos criar um provedor que registre os logs dos dados em uma tabela do SQLite, então antes de criá-lo precisamos preparar o ambiente.

Neste artigo estou utilizando uma aplicação ASP.NET Core 2.2. Então, após a criação dela, adicione a biblioteca do SQLite:

dotnet add package System.Data.SQLite

E do Dapper (caso queira, você pode optar por outra biblioteca ORM):

dotnet add package Dapper

Já a configuração desta conexão, está seguindo o mesmo padrão do meu artigo do Dapper, que você pode ver aqui. A diferença é que para o provedor será definida a entidade/model abaixo:

public class EventLog
{
    public int Id { get; set; }
    public string Category { get; set; }
    public int EventId { get; set; }
    public string LogLevel { get; set; }
    public string Message { get; set; }
    public DateTime CreatedTime { get; set; }
}

No próximo tópico veremos a implementação do repositório desta entidade.

Segundo Ato – Criando o provedor

Para criar um provedor para o LoggerFactory é necessário criar ao menos duas classes, que implementem, respectivamente, as interfaces ILogger e ILoggerProvider. Também é recomendado a criação de um método de extensão, seguindo o padrão Add{nome provedor}, que facilitará a sua adição.

E como implementaremos um provedor para o SQLite, implementaremos um repositório abstrato. Este repositório será uma propriedade da nossa classe “ILogger. Assim, de início defina este repositório:

public abstract class LoggerRepository : AbstractRepository<EventLog>
{
    public LoggerRepository(IConfiguration configuration) : base(configuration){}
}

Esta classe herda a classe AbstractRepository que demonstrei no artigo do Dapper.

Aproveitando, já defina a EventLogRepository:

public class EventLogRepository : LoggerRepository
{
    public EventLogRepository(IConfiguration configuration) : base(configuration){}

    public override void Add(EventLog item)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "INSERT INTO EventLog (Category, EventId, LogLevel, Message, CreatedTime)"
                            + " VALUES(@Category, @EventId, @LogLevel, @Message, @CreatedTime)";
            dbConnection.Open();
            dbConnection.Execute(sQuery, item);
        }
    }

    public override IEnumerable<EventLog> FindAll()
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Open();
            return dbConnection.Query<EventLog>("SELECT * FROM EventLog");
        }
    }

    public override EventLog FindByID(int id)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "SELECT * FROM EventLog" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            return dbConnection.Query<EventLog>(sQuery, new { Id = id }).FirstOrDefault();
        }
    }

    public override void Remove(int id)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "DELETE FROM EventLog" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            dbConnection.Execute(sQuery, new { Id = id });
        }
    }

    public override void Update(EventLog item)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "UPDATE EventLog SET Category = @Category,"
                        + " EventId = @EventId, LogLevel= @LogLevel," 
                        + " Message = @Message, CreatedTime= @CreatedTime" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            dbConnection.Query(sQuery, item);
        }
    }
}

Agora podemos definir a nossa classe ILogger, que será chamada SqliteLogger:

public class SqliteLogger<T> : ILogger where T: LoggerRepository
{
    private Func<string, LogLevel, bool> _filter;
    private T _repository;
    private string _categoryName;
    private readonly int maxLength = 1024;
    private IExternalScopeProvider ScopeProvider { get; set; }

    public SqliteLogger(Func<string, LogLevel, bool> filter, T repository, string categoryName)
    {
        _filter = filter;
        _repository = repository;
        _categoryName = categoryName;
    }

    public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;

    public bool IsEnabled(LogLevel logLevel) => (_filter == null || _filter(_categoryName, logLevel));

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        var logBuilder = new StringBuilder();

        if (!IsEnabled(logLevel)) 
        { 
            return; 
        } 
        if (formatter == null) 
        { 
            throw new ArgumentNullException(nameof(formatter)); 
        } 
        var message = formatter(state, exception);
        if (!string.IsNullOrEmpty(message)) 
        { 
            logBuilder.Append(message);
            logBuilder.Append(Environment.NewLine);
        }

        GetScope(logBuilder);

        if (exception != null)
            logBuilder.Append(exception.ToString());

        if (logBuilder.Capacity > maxLength)
            logBuilder.Capacity = maxLength;

        var eventLog = new EventLog 
        { 
            Message = message, 
            EventId = eventId.Id,
            Category = _categoryName,
            LogLevel = logLevel.ToString(), 
            CreatedTime = DateTime.UtcNow 
        };

        _repository.Add(eventLog);
    }

    private void GetScope(StringBuilder stringBuilder)
    {
        var scopeProvider = ScopeProvider;
        if (scopeProvider != null)
        {
            var initialLength = stringBuilder.Length;

            scopeProvider.ForEachScope((scope, state) =>
            {
                var (builder, length) = state;
                var first = length == builder.Length;
                builder.Append(first ? "=> " : " => ").Append(scope);
            }, (stringBuilder, initialLength));

            stringBuilder.AppendLine();
        }
    }
}

Note que esta é uma classe parametrizada, que define que o tipo de dados deve herdar a classe LoggerRepository:

public class SqliteLogger<T> : ILogger where T: LoggerRepository

No construtor da classe, é recebido um predicado, o repositório e a categoria:

public SqliteLogger(Func<string, LogLevel, bool> filter, T repository, string categoryName)
{
    _filter = filter;
    _repository = repository;
    _categoryName = categoryName;
}

O predicado é um filtro que definirá qual nível de log deve ser registrado. Já a categoria é o valor definido na criação do logger.

No método BeginScope, adicionamos os escopos definidos em um provider de escopo:

public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;

Esses escopos são iterados e posteriormente adicionados na mensagem que será salva no banco.

No método IsEnabled:

public bool IsEnabled(LogLevel logLevel) => (_filter == null || _filter(_categoryName, logLevel));

Verifica, de acordo com o filtro, se o log deve ser registrado ou não.

Já no método Log é onde montamos a nossa mensagem e a salvamos no banco de dados (através do repositório).

Com o SqliteLogger definido, podemos criar o provedor em si:

public class SqliteLoggerProvider<T>: ILoggerProvider where T : LoggerRepository
{
    private readonly Func<string, LogLevel, bool> _filter;
    private readonly T _repository;

    public SqliteLoggerProvider(Func<string, LogLevel, bool> filter, T repository)
    {
        this._filter = filter;
        this._repository = repository;
    }

    public ILogger CreateLogger(string categoryName) => new SqliteLogger<T>(_filter, _repository, categoryName);

    public void Dispose() {}
}

Esta classe também recebe no seu construtor um predicado de filtro e o repositório. Já no método CreateLogger é retornado uma instância do nosso logger, passando a categoria informada:

public ILogger CreateLogger(string categoryName) => new SqliteLogger<T>(_filter, _repository, categoryName);

Agora é necessário definir o método de extensão:

public static class SqliteLoggerExtensions
{
    public static ILoggingBuilder AddSqliteProvider<T>(this ILoggingBuilder builder, T repository) where T: LoggerRepository
    {
        builder.Services.AddSingleton<ILoggerProvider, SqliteLoggerProvider<T>>(p => new SqliteLoggerProvider<T>((_, logLevel) => logLevel >= LogLevel.Debug, repository));

        return builder;
    }
}

Neste método de extensão é definido que o level mínimo do log é o Debug, que é o mais baixo. Isso significa que todas as mensagens de log serão registrados.

Terceiro Ato – A hora da verdade

Com o provedor definido, podemos utilizá-lo. Na atual versão do ASP.NET Core (2.2), isso deve ser feito no método CreateWebHostBuilder da classe Program, conforme abaixo:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddSqliteProvider(new EventLogRepository(hostingContext.Configuration));
        });

Ao executar a aplicação, todos os logs do ASP.NET Core serão salvos no nosso banco de dados:

Qualquer log definido dentro da aplicação, como o abaixo:

public class HomeController : Controller
{
    private readonly ILogger _logger;

    public HomeController(ILogger<HomeController> logger) => _logger = logger;

    public IActionResult Index()
    {
        _logger.LogInformation("Chamando a página inicial do site");

        return View();
    }

    //...
}

Também será salvo no banco.

Epílogo

Mesmo com várias opções de provedores para o Microsoft.Extensions.Logging, às vezes há a necessidade de criar um provedor customizado. Felizmente, como vimos, a criação deste tipo de provedor não é complexa. Então, quando não achar uma solução que o atende, opte pela implementação customizada.

Você pode ver o código da aplicação utilizada neste artigo no meu Github, aqui.

Criando uma API RESTful com o NancyFx e .NET Core

Quando pensamos em API RESTful no C#, a primeira opção que vem a mente é o ASP.NET Web API. Ele é um ótimo framework, utilizado em vários sistemas, com uma abundante documentação e material de apoio. Mas ele não é a nossa única opção para a criação de API Rest.

Em artigos passados, conhecemos o ServiceStack, mas outra opção que está crescendo é o NancyFX.

Inspirado no framework Sinatra do Ruby, o Nancy é um framework de criação de serviços HTTP, leve e direto ao ponto. Tem por objetivo não atrapalhar o desenvolvedor. Ele obtém isso através de uma série de configurações padrões e convenções. Assim, com o Nancy é possível criar um site em minutos.

Claro que todas as suas convenções e configurações podem ser alteradas, caso este seja o desejo do desenvolvedor.

Para este artigo, mostrarei como é possível criar uma API simples, rapidamente utilizando o Nancy.

Criando a aplicação

Neste artigo estou utilizando o .NET Core, desta forma, mostrarei como criar a aplicação através do terminal. Inicialmente é necessário criar uma aplicação web vazia:

dotnet new web -n NancyAPI

No projeto criado, adicione a referência do Nancy:

dotnet add package Nancy --version 2.0.0-clinteastwood

Agora é necessário dizer ao ASP.NET que iremos utilizar o Nancy, para isso, faremos uso do OWIN.

O OWIN significa “Open Web Interface for .Net. Ele é um conjunto de padrões voltados para o .NET, que visa facilitar e encorajar a implementação de projetos que tentam desacoplar a aplicação do servidor. Na prática ele define um middleware ao pipeline de execução de uma aplicação ASP.NET.

Por se tratar de um middleware, deve ser configurado no método Configure da classe Startup:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseOwin(o => o.UseNancy());
}

Com isso, o Nancy já está ativo na nossa aplicação.

Módulos

No Nancy os endpoints devem ser definidos em módulos. Um módulo nada mais é que uma classe que herde a classe NancyModule. Esta classe pode ser definida em qualquer ponto do projeto, pois o Nancy fará uma busca pelos módulos quando um endpoint for invocado. O único ponto importante é que esta classe seja definida como pública (public) para que possa ser localizada.

Por exemplo:

using Nancy;

namespace NancyAPI.Module
{
    public class HomeModule : NancyModule
    {
        public HomeModule()
        {
            Get("/", _ => "Hello World from Nancy!");
        }
    }
}

Note que a rota está sendo definida como o primeiro parâmetro do método Get. Como é possível supor, esta rota será invocada quando for solicitada através de uma requisição GET.

O Nancy define métodos para os principais verbos do HTTP:

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

No segundo parâmetro do método deve ser passado a referência de uma função ( uma Function), que no exemplo acima, se utiliza lambda para definir a função. Este é o padrão recomendado pelo Nancy.

Ao executar a aplicação e acessar o endpoint definido, teremos o resultado:

Simples, não é?

Definindo a API RESTful

Para exemplificar uma API REST, vamos definir um modelo:

namespace NancyAPI.Models
{
    public class Pessoa
    {
        public int Id { get; set; }
        public string Nome { get; set; }
    }
}

E um repositório:

using System.Collections.Generic;
using NancyAPI.Models;
using System.Linq;

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

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

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

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

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

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

Agora só falta o nosso endpoint:

using Nancy;
using Nancy.ModelBinding;
using NancyAPI.Models;
using NancyAPI.Repositories;

namespace NancyAPI.Module
{
    public class PessoaModule: NancyModule
    {
        public readonly PessoaRepository repository;
        public PessoaModule()
        {
            repository = new PessoaRepository();

            Get("/pessoa/", _ => repository.GetAll());
            Get("/pessoa/{id}", args => repository.Get(args.id));
            Post("/pessoa/", args => {
                var pessoa = this.Bind<Pessoa>();

                repository.Add(pessoa);

                return pessoa;
            });
            Put("/pessoa/{id}", args => {
                var pessoa = this.Bind<Pessoa>();

                pessoa.Id = args.id;

                repository.Edit(pessoa);

                return pessoa;
            });
        }
    }
}

Neste endpoint é possível notar que os argumentos da URL são obtidos através do parâmetro do lambda:

Get("/pessoa/{id}", args => repository.Get(args.id));

Este parâmetro é um objeto dinâmico. Assim, o Nancy tentará obter o valor do argumento com base na propriedade informada.

Já os dados passados na solicitação, podem ser obtidos pelo Bind:

var pessoa = this.Bind<Pessoa>();

O Nancy irá converter automaticamente os dados obtidos para o tipo de objeto indicado.

Para testar os endpoints, podemos utilizar o Postman:

POST

GET

PUT

GET ID

Conclusão

Graças a sua estrutura simples, porém sofisticada, o Nancy tem atraído cada vez mais a atenção dos desenvolvedores. Se tornando uma ótima alternativa ao ASP.NET Web API.

Caso necessite criar uma API REST, não esqueça de dar uma boa olhada nele.

Você pode ver o projeto apresentado neste artigo no meu GitHub.

Documentando uma ASP.NET Core Web API com o Swagger

Com cada vez mais aplicações multiplataformas, é comum definir uma API REST que será consumida pelas várias versões da aplicação. Mas com vários times tendo acesso a API, não basta mais apenas desenvolvê-la e esperar que todos consigam utilizá-la institivamente.

Na época do web service, existia o WSDL que funcionava como uma documentação, já que facilitava a criação dos clientes que iria consumí-lo. Com as APIs REST não temos esta facilidade, assim, torna-se imprescindível que ela seja bem documentada.

Mas como realizar esta documentação? Existem algumas ferramentas que podem nos auxiliar neste processo, como: API Blueprint, RAML, Swagger, entre outras.

Como o nome deste artigo sugere, aqui abordaremos o Swagger.

Swagger

O Swagger é uma aplicação open source que auxilia os desenvolvedores a definir, criar, documentar e consumir APIs REST. Sendo uma das ferramentas mais utilizadas para esta função, a empresa por trás dela (a SmartBear Software), decidiu criar o Open API Iniciative e renomearam as especificações do Swagger para OpenAPI Specification.

Ela visa padronizar as APIs REST, desta forma descreve os recursos que uma API deve possuir, como endpoints, dados recebidos, dados retornados, códigos HTTP, métodos de autenticação, entre outros.

Para facilitar o processo de especificação/documentação de uma API, o Swagger fornece algumas ferramentas, como:

  • Swagger Editor: Editor que permite especificar uma API. Há uma versão online grátis e aplicativos para desktop;
  • Swagger UI: Interface interativa para a documentação da API;
  • Swagger Codegen: Gera templates de código (mais de 20 linguagens estão disponíveis) de acordo com uma especificação de API.

Neste artigo vamos focar na documentação de uma API existente, criada em ASP.NET Core Web API.

Especificação do Swagger

O ponto mais importante do Swagger é a sua especificação, que era chamada de Swagger specification e agora é OpenAPI Specification. Esta especificação trata-se de um documento, JSON ou YAML, que define a estrutura da API. Indicando os endpoints, formatos de entrada, de saída, exemplos de requisições, forma de autenticação, etc.

Abaixo você pode ver um exemplo desta especificação definida em JSON:

{
   "swagger": "2.0",
   "info": {
       "version": "v1",
       "title": "API V1"
   },
   "basePath": "/",
   "paths": {
       "/api/Example": {
           "get": {
               "tags": [
                   "Example"
               ],
               "operationId": "ApiExampleGet",
               "consumes": [],
               "produces": [
                   "text/plain",
                   "application/json",
                   "text/json"
               ],
               "responses": {
                   "200": {
                       "description": "Success",
                       "schema": {
                           "type": "array",
                           "items": {
                               "$ref": "#/definitions/Item"
                           }
                       }
                   }
                }
           },
           "post": {
               ...
           }
       },
       "/api/Example/{id}": {
           "get": {
               ...
           },
           "put": {
               ...
           },
           "delete": {
               ...
   },
   "definitions": {
       "Item": {
           "type": "object",
            "properties": {
                "id": {
                    "format": "int64",
                    "type": "integer"
                },
                "name": {
                    "type": "string"
                },
                "isValid": {
                    "default": false,
                    "type": "boolean"
                }
            }
       }
   },
   "securityDefinitions": {}
}

É possível definir esta especificação na mão, olhando as opções na documentação, através do Swagger Editor ou utilizando outra ferramenta para gerá-la.

No ASP.NET Core podemos utilizar duas bibliotecas para gerar este documento:

Por ser mais completa, faremos uso da Swashbuckle.

Adicionando o Swashbuckle na aplicação

Para este exemplo, estou utilizando uma aplicação ASP.NET Core Web API, baseada no exemplo fornecido pela documentação da Microsoft, que pode ser vista aqui. Você pode ver a aplicação que criei no meu Github.

Com a aplicação Web API criada, inicialmente é necessário adicionar o pacote do Swashbuckle na aplicação. Ele possui três componentes:

Mas é necessário instalar apenas o pacote Swashbuckle.AspNetCore. Por se tratar de uma aplicação .NET Core, este pacote pode ser adicionado com o comando abaixo:

dotnet add package Swashbuckle.AspNetCore

Após isso, para que o arquivo de especificação da API do Swagger seja criado, é necessário adicionar o gerador dele nos serviços da aplicação, no método ConfigureServices da classe Startup:

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

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });
    });
}

Para a classe Info, defina o using do namespace abaixo:

using Swashbuckle.AspNetCore.Swagger;

Agora, no método Configure é necessário ativar o Swagger e indicar o local onde o seu arquivo de especificação será criado:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //Ativa o Swagger
    app.UseSwagger();

    // Ativa o Swagger UI
    app.UseSwaggerUI(opt =>
    {
        opt.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI V1");
    });

    //... código omitido
}

Com isso, através de reflection a biblioteca consegue especificar os endpoints da aplicação.

Caso seja executada, podemos acessar o Swagger UI da aplicação no caminho /swagger:

Caso queira que o Swagger UI seja acessado a partir da raiz da aplicação, na ativação dela, no método Configure, atribua vazio para a propriedade RoutePrefix:

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI V1");
    c.RoutePrefix = string.Empty;
});

Customizando o Swagger

Como o Swashbuckle trabalha com reflection, podemos fazê-lo gerar mais informações das nossas APIs com base em configurações definidas nela.

Comentários de documentação

Sabemos que no C# podemos adicionar comentários de documentação nos nossos códigos, como:

/// <summary>
/// Lista os itens da To-do list.
/// </summary>
/// <returns>Os itens da To-do list</returns>
/// <response code="200">Returna os itens da To-do list cadastrados</response>
[HttpGet]
public ActionResult<List<Item>> Get()
{
    return _repository.GetAll();
}

É possível fazer o Swashbuckle ler esses comentários e assim tornar a especificação da API mais detalhada.

Para isso, na definição de geração do arquivo de especificação, no método ConfigureServices deve ser adicionado a informação abaixo:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);
});

Note que estamos indicando que ele deve ler os comentários do código a partir de um arquivo XML. Para que eles sejam gerados neste arquivo indicado, no arquivo de configuração do projeto (o .csproj), adicione o trecho abaixo:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

O <NoWarn> é definido para que a IDE não exiba um alerta sobre os códigos que não possuírem comentários de documentação.

Agora ao acessar o Swagger API, o que foi definido em <summary> será mostrado:

Também podemos o <remarks> para indicar um exemplo de requisição:

// POST api/todo
/// <summary>
/// Cria um item na To-do list.
/// </summary>
/// <remarks>
/// Exemplo:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item1",
///        "iscomplete": true
///     }
///
/// </remarks>
/// <param name="value"></param>
/// <returns>Um novo item criado</returns>
/// <response code="201">Retorna o novo item criado</response>
/// <response code="400">Se o item não for criado</response>        
[HttpPost]
public ActionResult<Item> Post([FromBody] Item value)
{
    Console.WriteLine(value?.Name);

    var item = _repository.Save(value);
    if(item != null)
        return item;

    return BadRequest();
}

O resultado será:

Com as anotações ProducesResponseType e Produces podemos indicar os códigos HTTP e o tipo de dado que os endpoints irão retornar:

[Authorize]
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
    //..código omitido

    // GET api/todo
    /// <summary>
    /// Lista os itens da To-do list.
    /// </summary>
    /// <returns>Os itens da To-do list</returns>
    /// <response code="200">Returna os itens da To-do list cadastrados</response>
    [HttpGet]
    [ProducesResponseType(200)]
    public ActionResult<List<Item>> Get()
    {
        return _repository.GetAll();
    }

    //... código omitido
}

Especificando a autenticação

Uma das principais vantagens de adicionar o Swagger na aplicação, é poder testar os endpoints pela Swagger UI. Quando a aplicação define autenticação, é necessário indicar isso para o Swashbuckle, para que ele também gere uma especificação desde detalhe.

Com isso, será possível testar os endpoints pela Swagger UI utilizando a autenticação definida.

Para fazer isso, devemos adicionar a informação abaixo, na definição de geração do arquivo de especificação, no método ConfigureServices:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info { Title = "TodoAPI", Version = "v1" });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);

    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };

    opt.AddSecurityDefinition(
        "Bearer",
        new ApiKeyScheme
        {
            In = "header",
            Description = "Copie 'Bearer ' + token'",
            Name = "Authorization",
            Type = "apiKey"
        });

    opt.AddSecurityRequirement(security);
});

Acima estamos definindo que a aplicação faz uso de autenticação do tipo JWT Token (“Bearer Token”). Na documentação do Swagger, você pode ver os outros tipos de autenticação suportados.

Ao acessar o Swagger UI, ele irá mostrar o botão “Autorize”:

Exibe um botão com o texto "Autorize"

Ao clicar nele, é possível informar o tipo de autenticação especificado:

Com isso, mesmo que necessite de autenticação, será possível realizar as requisições dos endpoints da aplicação pelo Swagger UI.

Descrição

Por fim, é possível descrever um pouco da aplicação através das propriedades da classe Info da especificação do Swagger:

services.AddSwaggerGen(opt =>
{
    opt.SwaggerDoc("v1", new Info
    {
        Version = "v1",
        Title = "Todo API",
        Description = "Um exemplo de aplicação ASP.NET Core Web API",
        TermsOfService = "Não aplicável",
        Contact = new Contact
        {
            Name = "Wladimilson",
            Email = "contato@treinaweb.com.br",
            Url = "https://treinaweb.com.br"
        },
        License = new License
        {
            Name = "CC BY",
            Url = "https://creativecommons.org/licenses/by/4.0"
        }
    });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    opt.IncludeXmlComments(xmlPath);

    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };

    opt.AddSecurityDefinition(
        "Bearer",
        new ApiKeyScheme
        {
            In = "header",
            Description = "Copie 'Bearer ' + token'",
            Name = "Authorization",
            Type = "apiKey"
        });

    opt.AddSecurityRequirement(security);
});

O resultado será:

Swagger UI mostrando a descrição da API

Conclusão

O Swagger é uma ótima forma de documentar APIs REST e com a biblioteca Swashbuckle, podemos gerar o arquivo de especificação do Swagger com facilidade. Assim, não há mais desculpa para se definir documentações interativas e detalhadas para as suas APIs REST.

Compreendendo os middlewares no ASP.NET Core

Desde a sua primeira versão, o ASP.NET faz uso dos middlewares. Eles foram implementados como uma forma de modularizar uma aplicação ASP.NET facilmente.

Em termos práticos, middleware seria um trecho de código que pode ser executado no fluxo de execução da aplicação. No ASP.NET os middleware são organizados em um pipeline e são executados conforme uma solicitação é recebida e uma resposta enviada. A imagem abaixo ilustra este pipeline:

A imagem apresenta uma solicitação sendo processada por três middlewares, sendo que cada um chamado o middleware seguinte. No terceiro middleware, uma resposta é retornada e esta passa pelos middleware chamados anteriormente.

Cada middleware pode executar uma ação no recebimento da solicitação, chamar o próximo middleware, utilizando o método next(), e executar outra ação durante o retorno da resposta. Só não é possível modificar a resposta no seu retorno.

Dependendo da funcionalidade, o middleware pode decidir não chamar o próximo no pipeline, não invocando o método next(). Por exemple, o middleware de arquivos estáticos pode retornar uma solicitação para um arquivo estático e interromper o fluxo restante.

É possível definir middleware de diversas funcionalidades, por exemplo, no trecho de código abaixo, temos oito middlewares:

  1. Exception/error handling
  2. HTTP Strict Transport Security Protocol
  3. HTTPS redirection
  4. Static file server
  5. Cookie policy enforcement
  6. Authentication
  7. Session
  8. MVC
public void Configure(IApplicationBuilder app)
{
    if (env.IsDevelopment())
    {
        // Quando executado em desenvolvimento:
        //   Utiliza Developer Exception Page para reportar erros.
        //   Utiliza Database Error Page para reportar erros do banco.
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        // Quando não estiver em produção:
        //   Habilita o middleware Exception Handler Middleware para pegar os erros.
        //   Utiliza o middleware que habilita o 
        //       HTTP Strict Transport Security Protocol (HSTS)
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    // Utiliza o middleware HTTPS Redirection que redireciona solicitações HTTP para HTTPS.
    app.UseHttpsRedirection();

    // Retorna arquivos estáticos e interrompe o pipeline.
    app.UseStaticFiles();

    // Utiliza o middleware Cookie Policy, que está em conformidade com 
    // as regras do GDPR (General Data Protection Regulation).
    app.UseCookiePolicy();

    // Autentica antes de utilizar os recursos.
    app.UseAuthentication();

    // Se o aplicativo utiliza sessão, chama o middleware Session depois do middleware 
    // Cookie Policy e antes do middleware MVC.
    app.UseSession();

    // Adiciona MVC ao pipeline da solicitação
    app.UseMvc();
}

Como é possível notar, no ASP.NET todos os middlewares são definidos no método Configure no padrão Use* seguindo do nome do middleware. Neste método também é possível adicionar middlewares customizados, utilizando os métodos Use ou Run. A diferença entres eles é que os middlewares definidos com Run são middlewares “finais”, após eles, nenhum outro middleware é chamado.

Entendendo a ordem do pipeline

Para compreender a ordem de execução dos middlewares no pipeline, vamos definir alguns middlewares simples:

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

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

Neste exemplo os middlewares estão sendo definidos como métodos anônimos, em um artigo futuro mostrarei as formas de declará-los.

Neste ponto o importante é que sabia que os middlewares definidos com Use recebem dois parâmetros: a instância de um objeto HttpContext e a instância do delegate RequestDelegate, que aponta para o próximo middleware no pipeline.

O resultado do código acima será algo assim:

Primeiro middleware (antes)
Segundo middleware (antes)
Middleware final
Segundo middleware (depois)
Primeiro middleware (depois)

Note que a ordem que esses middlewares estiverem definidos no método Configure irá indicar a ordem que eles serão chamados no pipeline. Ao chegar o último middleware do pipeline, os middlewares anteriores são chamados novamente.

Short-circuiting middleware

Caso algum dos middlewares não invocar o método next(), o pipeline será encerrado antes do seu final:

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

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        // await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

O resultado será:

Primeiro middleware (antes)
Segundo middleware (antes)
Segundo middleware (depois)
Primeiro middleware (depois)

Um middleware não final, que não invoca o método next, é chamado de short-circuiting middleware.

Dividindo o pipeline

Nos exemplos anteriores, o nosso pipeline possuía apenas um fluxo: o primeiro middleware sempre vem antes do segundo e por fim é chamado o middleware final. Mas não precisa ser sempre desta forma. Dependendo da solicitação pode ser definido um fluxo diferente para o pipeline.

Criando novo fluxo final

Com os métodos Map ou MapWhen é possível definir um novo fluxo final para o pipeline. O método Map permite especificar um middleware que será invocado de acordo com o caminho da solicitação. Já o MapWhen possui mais poder porque o padrão pode ser definido utilizando o objeto HttpContext:

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

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Map("/foo",
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /foo (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /foo (depois) ");
            });
    });

    app.MapWhen(context => context.Request.Path.StartsWithSegments("/bar"), 
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /bar (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /bar (depois) ");
            });
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

Mesmo sendo possível invocar o método next() nos middlewares definidos com Map e MapWhen, por não existir nenhum middleware seguinte, ao ser invocado é gerado um erro. Por exemplo, se a solicitação possuir um caminho /foo será gerado um fluxo assim:

Lembrando que também há a volta.

Se os métodos next() forem omitidos dos middlewares definidos em Map e MapWhen, teríamos um fluxo assim:

Primeiro middleware (antes)
Segundo middleware (antes)
Middleware para o caminho /foo (antes)
Middleware para o caminho /foo (depois)
Segundo middleware (depois)
Primeiro middleware (depois)

Criando um fluxo alternativo no pipeline

Para evitar o problema do uso do next() nos middlewares definidos em Map e MapWhen, é possível utilizar o método UseWhen, que funciona da mesma forma que o MapWhen, com a diferença que após executá-lo o fluxo do pipeline retorna ao caminho padrão:

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

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Segundo middleware (antes)");
        await next();
        await context.Response.WriteAsync("Segundo middleware (depois)");
    });

    app.Map("/foo",
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /foo (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /foo (depois) ");
            });
    });

    app.UseWhen(context => context.Request.Path.StartsWithSegments("/bar"), 
        (a) => {
            a.Use(async (context, next) => {
                await context.Response.WriteAsync("Middleware para o caminho /bar (antes) ");
                await next();
                await context.Response.WriteAsync("Middleware para o caminho /bar (depois) ");
            });
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Middleware final");
    });
}

Caso a requisição tenha o caminho /bar, o resultado do pipeline será:

Primeiro middleware (antes)
Segundo middleware (antes)
Middleware para o caminho /bar (antes)
Middleware final
Middleware para o caminho /bar (depois)
Segundo middleware (depois)
Primeiro middleware (depois)

Podemos ilustrar isso na imagem abaixo:

Conclusão

Como é possível ver, a ideia por trás dos middlewares no ASP.NET Core é simples, porém muito poderosa. Muitos recursos do framework são disponibilizados para as aplicações através de middlewares.

Por isso o conhecimento do funcionamento do seu pipeline é um requisito obrigatório de todo desenvolvedor ASP.NET Core.

Otimização adaptativa no .NET Core 2.1

Ao desenvolver um sistema utilizando uma linguagem de alto nível, sempre teremos o custo do processo de compilação. Mesmo que não ganhe muito destaque, no final este será o processo que definirá a performance da aplicação.

Linguagens consideradas “mais rápidas”, como C, geram aplicações mais performáticas porque durante o processo de compilação estática são realizadas várias otimizações, visando o ambiente definido. Este é um dos motivos que em ambiente Unix drivers, bibliotecas e outras aplicações, as vezes precisam ser compiladas localmente antes de serem instaladas no sistema.

Em linguagens de alto nível, geralmente o código não é compilado diretamente para a linguagem de máquina. Ele é compilado para uma camada intermediária e dela para a linguagem de máquina. Como pode ser ilustrado pela arquitetura do .NET demostrado na imagem abaixo:

Neste tipo de arquitetura, esta camada intermediária as vezes recebe o nome de máquina virtual, já que pode emular um ambiente, o responsável por executar o código intermediário da aplciação, em qualquer sistema. Um exemplo clássico disso é a Java Virtual Machine (JVM).

E será esta máquina virtual que definirá a performance da aplicação. Quanto mais eficiente for, mais performáticas podem ser as aplicações desenvolvidas. Por esses motivos que existem máquinas virtuais, como a HHVM, que tem por objetivo otimizar as aplicações desenvolvidas em PHP (com o fim do suporte agora em 2018) e Hack.

Entrando no .NET, nele não há uma máquina virtual clara, mas como é possível ver na imagem da arquitetura acima, ele possui uma camada que é responsável por intermediar a execução do código intermediário em código de máquina.

CoreCLR

Ao desenvolvermos uma aplicação em qualquer linguagem suportada pelo .NET Core, antes dela ser executada, o código é convertido para uma linguagem intermediária (a MSIL, que traduzido literalmente significa linguagem intermediária), e o CoreCLR ficará responsável por converter este código IL em linguagem de máquina.

É esta camada que realiza todo o processo de otimização das aplicações .NET Core. Além que garantir que o código é seguro, não gera vazamento de memória, entre outros recursos. Geralmente ele tem que decidir se irá gerar um código de rápida inicialização, com pouca otimização, ou se irá implementar uma grande otimização, atrasando a sua inicialização.

Sendo um sucessor do CLR do .NET Framework, o CoreCLR tomava esta decisão uma vez, pois se o código de máquina de um método já estivesse gerado, este sempre seria utilizado quando este método fosse referenciado no código.

A primeira vista, otimizar todos os métodos parece ser a melhor opção, mas em alguns casos, o método pode ser pouco utilizado, que não compensa o custo de otimização.

Visando melhorar a performance das aplicações .NET Core, na versão 2.1 desta biblioteca, o CoreCLR foi motificado, adicionando o suporte a tiered compilation (compilação em camadas), também conhecida como otimização adaptativa.

Tiered Compilation

Na otimização adaptiva a máquina virtual tem mais opções. Ela pode gerar um código “sujo” de rápida inicialização, e se notar que o método é muito utilizado, posteriormente pode otimizá-lo para obter máxima performance.

Na implementação deste recurso no CoreCLR, isso significa que agora um método pode ter várias compilações que podem ser substituídas durante a execução da aplicação. Desta forma, pode-se obter por uma rápida inicialização ou por otimização:

  • Rápida inicialização: Quando uma aplicação é iniciada, ela aguarda o código intermediário a ser convertido em linguagem de máquina. A tiered compilation pede para ser gerada uma compilação inicial rapidamente, mesmo que seja necessário sacrificar a qualidade da otimização. Caso o método seja chamado frequentemente, um código mais otimizado é gerado em background, e ele substituí o código gerado inicialmente, para se obter mais performance.

  • Otimização: Em uma aplicação .NET típica, a maioria do código da biblioteca é gerado de imagens pré-compiladas. O que é muito bom para uma rápida inicialização, mas as imagens pré-compiladas possuem restrições de versões e instruções de CPU que limitam a otimização. Para cada método dessas imagens que são frequentemente chamados, a tiered compilation pede para ser gerada em background uma nova compilação mais otimizada, que substituirá a versão pré-compilada.

Resultados

Você pode estar se perguntando, o quão rápido uma aplicação pode ser com este tipo de compilação. Nos testes da equipe do .NET, eles obtiveram uma melhoria de uns 35%. Em alguns casos até mais, como é possível ver nos gráficos abaixo:

Em aplicações que utilizamos internamente aqui na TreinaWeb, obtivemos uma melhora de cerca de 40%.

Testando nas suas aplicações

No momento, este recurso está em fase beta. Caso esteja utilizando a última versão de preview do .NET Core 2.2, ele já está ativo por padrão, mas no .NET Core 2.1 é necessário ativá-lo no seu ambiente.

Caso queria ativar a tiered compilation para todas as suas aplicações, adicione nas variáveis de ambiente do seu sistema a variável abaixo:

COMPlus_TieredCompilation="1"

Para ativá-la em uma aplicação que esteja sendo desenvolvida, adicione no arquivo de configuração do projeto a tag TieredCompilation:

<PropertyGroup>
   <!-- outras definições -->

   <TieredCompilation>true</TieredCompilation>
</PropertyGroup>

Já para um projeto que que esteja rodando, você pode adicionar o atributo System.Runtime.TieredCompilation no arquivo configProperties dele:

{
    "runtimeOptions": {
        "configProperties": {
            "System.Runtime.TieredCompilation": true
        }
    },
    "framework": {
        ...
    }
}

Conclusão

A otimização adaptativa ainda está em fase beta no .NET Core 2.1, mas devido aos seus beneficios, é um recurso que vale a pena ser testado.

Caso encontre qualquer bug, não hesite em informar para a equipe do CoreCLR na sua página no git.

Novos recursos do ASP.NET Core 2.1

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

Criando a aplicação

Crie a aplicação com o comando abaixo:

dotnet new mvc -n AspNetCore2_1Exemplo -au Individual

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

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

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

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

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

app.UseCookiePolicy();

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

app.UseHttpsRedirection();

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

@using Microsoft.AspNetCore.Http.Features

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

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

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

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

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

Conclusão

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

Global Tools no .NET Core 2.1

Neste artigo vou falar sobre as Global Tools, que é um recurso adicionado na versão 2.1 do .NET Core.

Uma global tool é um pacote “especial” do NuGet que contém uma aplicação console. Quando esta aplicação é instalada como uma ferramenta, o .NET Core CLI irá baixar o seu arquivo, extrair no disco e disponibilizá-la como um comando de terminal.

Nos bastidores, o .NET Core CLI está apenas adicionando o caminho da aplicação na variável de ambiente PATH do sistema. E por ser executada pelo terminal, que este recurso só é valido para as aplicações console.

Aqui não existe limitação do que a aplicação pode implementar. Ou seja: caso a aplicação console esteja executando corretamente, ela pode ser convertida para uma “global tool”.

Para entender isso na prática, vamos criar uma aplicação simples de renomeação de arquivos.

Ambiente

Antes de colocarmos a mão na massa, é importante que a versão 2.1 (ou superior) do .NET Core esteja instalada na máquina.

Para este artigo, estou utilizando a versão 2.1.4 do .NET Core, em uma máquina Mac OS X.

Colocando a mão na massa

Como dito, uma global tool é criada a partir de uma aplicação console, assim, a primeira coisa que deve ser feita é criar esta aplicação:

dotnet new console -n RenomearArquivos

Em seguida o seu código deve ser definido. No nosso exemplo, ele será:

class Program
{
    static void Main(string[] args)
    {
        if(args.Count() == 0)
            Console.WriteLine("Nenhuma opção foi informada! Indique o diretório (-d), filtro (-t) e prefixo (-n)");
        else
        {
            var diretorio = String.Empty;
            var filtro = "*";
            var prefixo = String.Empty;

            for(var i = 0; i < args.Count(); i++)
            {
                switch (args[i].ToLower())
                {
                    case "-d":
                        diretorio = args[++i];
                        break;
                    case "-t":
                        filtro = args[++i];
                        break;
                    case "-n":
                        prefixo = args[++i];
                        break;
                    default:
                        Console.WriteLine($"Opção {args[i]} inválida!");
                        Environment.Exit(1);
                        break;
                }
            }

            Renomear(diretorio, filtro, prefixo);
        }

    }

static void Renomear(string diretorio, string filtro, string prefixo){

        Console.WriteLine("Arquivos renomeados:");
        foreach(var arq in Directory.GetFiles(diretorio, filtro))
        {
            var novoArquivo = Path.Combine(Path.GetDirectoryName(arq), String.Format("{0} - {1}", prefixo, Path.GetFileName(arq)));
            File.Move(arq, novoArquivo);
            Console.WriteLine(novoArquivo);
        }
    } 
} 

Como é possível notar, esta aplicação é bem simples.

Ao executá-la, veremos o seguinte resultado:

Mas como disse, qualquer aplicação console pode ser transformada em uma global tool. Infelizmente, ainda não há uma página que organize todas as global tools disponíveis, mas aqui contém ótimos exemplos de ferramentas criadas pela equipe do ASP.NET Core.

Empacotando a aplicação

Para disponibilizar a nossa aplicação como uma global tool precisamos empacotá-la em um pacote NuGet. Para isso, temos de adicionar algumas informações no arquivo de configuração do projeto (*.csproj):

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.1</TargetFramework>
  <PackAsTool>true</PackAsTool>
  <ToolCommandName>renarquivos</ToolCommandName>
  <Version>1.0.0</Version>
  <Description>A example of a tool for the Treinaweb Blog</Description>
</PropertyGroup>

Onde:

  • PackAsTool: Indica que a aplicação deve ser empacotada como uma global tool;
  • ToolCommandName: Indica o comando utilizado para invocar a aplicação. Caso isso não seja especificado, será utilizado o nome da aplicação;
  • Version: A versão da aplicação;
  • Description: Descrição da ferramenta.

E, deve ser criado na raiz da aplicação um arquivo chamado .nuspec onde deve ser adicionado a informação abaixo:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <!-- Esta parte é obrigatória -->
    <packageTypes>
      <packageType name="DotnetTool" />
    </packageTypes>
    <!-- ... -->
  </metadata>
</package>

Ela indica que o pacote a será criado deve ser tratado como uma global tool. Sem esta informação, o pacote até pode ser criado, mas não seria possível instalá-lo como uma global tool.

Com essas informações definidas, a aplicação pode ser empacotada com o comando abaixo:

dotnet pack . -c Release -o nupkg

Este comando irá compilar a aplicação para “release”, e irá gerar o arquivo de pacote dela na pasta nupkg. Para testar a ferramenta, acesse a pasta nupkg e execute o comando abaixo:

dotnet new nugetconfig

Este comando irá gerar um arquivo de configuração do NetGet (nuget.config) onde deve ser informando o caminho do arquivo NuGet que acabamos de gerar:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <packageSources>
    <add key="local" value="." />
 </packageSources>
</configuration>

Desta forma, sempre que o comando de edição de pacotes for executado no nível desta pasta, ele também irá procurar por pacotes contidos nela.

Instalado a Global Tool

Por fim, para instalar a global tool que acabamos de definir, basta executar no nível da sua pasta, o comando abaixo:

dotnet tool install -g RenomearArquivos

Após isso, a nossa global tool estará disponível como comando no terminal:

Simples não, é? O código desta aplicação está disponível aqui.

Em artigos futuros falarei de outros recursos da versão 2.1 do .NET Core. Até lá!

Criando uma aplicação distribuída com ASP.NET Core e o Microsoft Orleans

Este é mais um artigo de uma série sobre o Microsoft Orleans. Caso não tenha visto o primeiro, recomendo que o leia: Conhecendo o Microsoft Orleans.

Como dito no artigo passado, o Orleans está na segunda versão e uma das novidades dela é o suporte ao .NET Standard 2.0, desta forma, como o título indica, a aplicação demonstrada aqui utilizará este framework.

Estrutura da solução

Antes de colocarmos a mão na massa, é importante entender como funciona a solução do Orleans. Esta biblioteca recomenda que um projeto que a implemente seja estruturado com no mínimo quatro projetos:

  • OrleansHost: Este projeto irá criar um executável que iniciará os silos, os hosts do Orleans que conterá as instâncias dos grãos. É possível iniciar vários silos, que irão trabalhar em conjunto e compartilhar a carga.

  • GrainsInterfaces: Este projeto contém as interfaces que definem todos os grãos contido nos silos.

  • Grains: Este projeto contém a implementação de todos os grãos definidos em GrainsInterfaces. Por isso, ele é implementado em conjunto com o OrleansHost.

  • Inteface: Este projeto pode ser qualquer projeto de interface, mas geralmente trata-se de API. Ele irá se conectar ao OrleansHost, por TCP, para ter acesso aos grãos.

Durante o desenvolvimento, o projeto interface e o OrleansHost podem ser executados em conjunto na mesma máquina. Mas em produção, geralmente eles são executados separadamente. O OrleansHost é implementado em um cluster e a interface em um servidor web.

Neste artigo, a solução será feita em uma máquina Mac OSX, utilizando a versão 2.1.4, em conjunto com o Visual Studio Code, pois amo este ambiente. Mas o Orleans fornece um plugin de templates para o Visual Studio, que facilita a criação de projetos com esta biblioteca. Desta forma, caso esteja utilizando esta IDE recomendo que instale este plugin.

Criando o projeto GrainsInterfaces

Como o projeto Grains necessita do GrainsInterfaces e o OrleansHost fará uso do Grains, o primeiro projeto que precisa ser criado é o GrainsInterfaces.

Caso esteja utilizando o Visual Studio com o plugin do Orleans instalado, crie um projeto com base no template “Orleans Grain Interface Collection. No meu ambiente, incialmente irei criar uma pasta chamada OrleansDotNet, e dentro dela uma solução:

dotnet new sln -n OrleansDotNet

Em seguida, um projeto Class Library:

dotnet new classlib -n GraosInterfaces

Neste projeto adicione a biblioteca Microsoft.Orleans.OrleansCodeGenerator.Build:

dotnet add package Microsoft.Orleans.OrleansCodeGenerator.Build

E adicione nele a interface abaixo:

using System;
using Orleans;
using System.Threading.Tasks;

namespace OrleansDotNet.GraosInterfaces
{
    public interface IGraoContador: IGrainWithStringKey
    {
        Task Incremento(int incremento);
        Task<int> GetContador();
    }
}

As interfaces grãos devem definir a implementação de uma das interfaces “GrainWith*” do Orleans e definir métodos que retornem uma Task, para métodos void, ou Task, caso o método retorne algum valor.

A interface implementada acima, IGrainWithStringKey, é utilizada para indicar que o código de identificação do grão será definido com uma string. Infelizmente a documentação do Orleans ainda não documenta bem todas as interfaces disponíveis, mas em um artigo futuro abordo todas.

Criando o projeto Grains

Com a interface definida podemos implementá-la um um projeto Grains. Caso esteja utilizando o Visual Studio, crie um projeto com o template Orleans Grain Class Collection. No meu ambiente, irei criar um projeto Class Library chamado Graos:

dotnet new classlib -n Graos

E este projeto também precisa da referência abaixo:

dotnet add package Microsoft.Orleans.OrleansCodeGenerator.Build

Em seguida, referencie o projeto de interface:

dotnet add Graos.csproj reference ../GraosInterfaces/GraosInterfaces.csproj

Agora podemos implementar o nosso grão:

using System;
using Orleans;
using System.Threading.Tasks;
using OrleansDotNet.GraosInterfaces;

namespace OrleansDotNet.Graos
{
    public class GraoContador: Grain, IGraoContador
    {
        private int _contador;

        public Task Incremento(int incremento)
        {
            _contador += incremento;
            return Task.CompletedTask;
        }

        public Task<int> GetContador()
        {
            return Task.FromResult(_contador);
        }
    }
}

Esta classe não tem segredo, o importante é implementar a nossa interface grão e herdar a classe Grain. Dentro dela é definido um contador simples.

Criando o projeto OrleansHost

Com o grão definido, podemos definir o nosso silo (host). Caso esteja utilizando o Visual Studio, crie um projeto com base no template “Orleans Dev/Test Host“.

No meu ambiente irei criar uma aplicação console chamada Silo:

dotnet new console -n Silo

Nele adicione adicione uma referência para a biblioteca Microsoft.Orleans.Server:

dotnet add package Microsoft.Orleans.Server

Como iremos registrar o log do silo do console, adicione a referencia abaixo:

dotnet add package Microsoft.Extensions.Logging.Console

Por fim, adicione a referencia do projeto Graos:

dotnet add Silo.csproj reference ../Graos/Graos.csproj

E na classe Program adicione o código abaixo:

using System;
using Orleans;
using Orleans.Runtime.Configuration;
using Orleans.Hosting;
using Orleans.Configuration;
using System.Threading.Tasks;
using OrleansDotNet.Graos;
using Microsoft.Extensions.Logging;
using System.Runtime.Loader;
using System.Threading;
using System.Net;

namespace OrleansDotNet.Silo
{
    class Program
    {
        private static ISiloHost silo;
        private static readonly ManualResetEvent siloStopped = new ManualResetEvent(false);

        static void Main(string[] args)
        {

            silo = new SiloHostBuilder()
                .UseLocalhostClustering()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "OrleansDotNet-cluster";
                    options.ServiceId = "OrleansDotNet";
                })
                .Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
                .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(GraoContador).Assembly).WithReferences())
                .ConfigureLogging(logging => logging.AddConsole())
                .Build();

            Task.Run(StartSilo);

            AssemblyLoadContext.Default.Unloading += context =>
            {
                Task.Run(StopSilo);
                siloStopped.WaitOne();
            };

            siloStopped.WaitOne();

        }

        private static async Task StartSilo()
        {
            await silo.StartAsync();
            Console.WriteLine("Silo iniciado");
        }

        private static async Task StopSilo()
        {
            await silo.StopAsync();
            Console.WriteLine("Silo parado");
            siloStopped.Set();
        }
    }
}

Nesta classe, estamos definindo que o Silo irá utilizar as configurações de um cluster local (UseLocalhostClustering()), também é definido as identificações do cluster, bem com o seu IP (Configure()). Por fim, se define o grão que será adicionado ao silo e onde o seu log deve ser exibido.

Com o silo definido, ele é iniciado. Ele só irá parar em caso de algum erro ou quando o usuário forçar a sua parada.

Criando o projeto Interface

Com o Silo definido podemos criar a nossa aplicação interface. Neste projeto vou criar uma aplicação WebAPI chamada InterfaceAPI:

dotnet new webapi -n InterfaceApi

Aproveite e já vincule os projetos a solução:

dotnet sln OrleansDotNet.sln add **/*.csproj

Na api é necessário adicionar a referência Microsoft.Orleans.Client:

dotnet add package Microsoft.Orleans.Client

E referenciar o projeto GraosInterfaces:

dotnet add InterfaceApi.csproj reference ../GraosInterfaces/GraosInterfaces.csproj

Para trabalhar com “dependency injection“, vamos configurar o cliente do Orleans na classe Startup conforme o código abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Orleans;
using Orleans.Runtime.Configuration;
using Orleans.Hosting;
using System.Net;
using Orleans.Configuration;
using OrleansDotNet.GraosInterfaces;

namespace InterfaceApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddSingleton<IClusterClient>(provider =>
            {
                var client = new ClientBuilder()
                            .UseLocalhostClustering()
                            .Configure<ClusterOptions>(options =>
                            {
                                options.ClusterId = "OrleansDotNet-cluster";
                                options.ServiceId = "OrleansDotNet";
                            })
                            .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IGraoContador).Assembly).WithReferences())
                            .ConfigureLogging(logging => logging.AddConsole())
                            .Build();

                StartClientWithRetries(client).Wait();

                return client;
            });
        }

        private static async Task StartClientWithRetries(IClusterClient client)
        {
            for (var i=0; i<5; i++)
            {
                try
                {
                    await client.Connect();
                    return;
                }
                catch(Exception)
                { }
                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
            app.UseStaticFiles();
        }
    }
}

Note que a configuração do cliente é parecida com o do host:

var client = new ClientBuilder()
            .UseLocalhostClustering()
            .Configure<ClusterOptions>(options =>
            {
                options.ClusterId = "OrleansDotNet-cluster";
                options.ServiceId = "OrleansDotNet";
            })
            .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IGraoContador).Assembly).WithReferences())
            .ConfigureLogging(logging => logging.AddConsole())
            .Build();

As diferenças é que agora estamos utilizando a classe ClientBuilder e se adiciona a interface IGraoContador (em detrimento a GraoContador definida no host.

Nesta classe também foi definido o método StartClientWithRetries que tenta se conectar ao host 5 vezes antes de desistir. Quando a conexão for obtida, o objeto client é retornado.

Este cliente será obtido no controller, conforme o código abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using OrleansDotNet.GraosInterfaces;

namespace InterfaceApi.Controllers
{
    [Route("api/[controller]")]
    public class ContadorController : Controller
    {
        private IClusterClient client;

        public ContadorController(IClusterClient client){
            this.client = client;
        }

        [HttpGet]
        public async Task<int> Get()
        {
            var contador = client.GetGrain<IGraoContador>("TW-1");

            return await contador.GetContador();
        }


        [HttpPost]
        public async Task Post()
        {
            var contador = client.GetGrain<IGraoContador>("TW-1");
            await contador.Incremento(1);
        }
    }
}

Para obter o grão, utilizamos o método GetGrain. A este método deve ser passado a chave do grão:

var contador = client.GetGrain<IGraoContador>("TW-1");

Como definimos que a chave dele será uma string, acima estou passando uma string arbitrária. A partir da instância obtida os métodos do grão são chamados, como o GetContador():

return await contador.GetContador();

Com isso, nós estamos obtendo informações do nosso grão contido no silo.

Para finalizar, criei dentro da pasta wwwroot um arquivo HTML, onde os métodos da api são chamados:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">


    <title>Exemplo de Orleans, no ASP.NET Core *.*</title>
</head>
<body>
    <div class="container">
        <div class="card">
            <div class="card-body">
                Contador: <spam id="countValor">0</spam>

                <button id="btnIncrement" class="btn-primary">Incrementar</button>
            </div>
        </div>


    </div>


    <!-- JavaScript -->
    <script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>

    <script type="text/javascript">
        $(document).ready(function () {
            $.get("api/contador/", function (value) {
                console.log('GET contador=' + value);

                $('#countValor').html(value);
            });

            $('#btnIncrement').click(function () {
                $.post("api/contador/", function () {
                    $.get("api/contador/", function (value) {
                        console.log('GET contador=' + value);

                        $('#countValor').html(value);
                    });
                });
                return false;
            });
        });
    </script>
</body>
</html>

Pronto, a nossa aplicação está finalizada. Para testá-la, primeiro é necessário iniciar o Silo:

Em seguida a aplicação web:

No navegador teremos o resultado abaixo:

Ao clicar em “Incrementar“, veremos o valor do contador ser incrementado:

Conclusão

Este é um exemplo simples, porém funcional, que nos dá uma noção do poder do Orleans. Em artigos futuros mostrarei mais detalhes dele.

Caso queria executar a aplicação demostrada aqui no curso, você pode vê-la aqui no GitHub.

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES