Criando um middleware customizado para ASP.NET Core

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

Definindo o middleware customizado diretamente no pipeline

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

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

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

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

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

Definindo uma classe “Middleware”

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

public class MyMiddleware
{
    private readonly RequestDelegate _next;

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

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

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

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

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

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

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

E utilizá-lo no Configure:

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

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

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

Definindo configurações para o Middleware

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

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

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

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

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

        service.Configure(options);
        return service;
    }

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

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

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

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

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

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

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

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

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

E indicá-las ConfigureServices:

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

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

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

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

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

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

E modificar o construtor do middleware:

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

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

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

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

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

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

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

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

Conclusão

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

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.