Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 5 de febrero de 2019
ASP.NET Core Tradicionalmente los middlewares de ASP.NET Core los hemos implementado como clases independientes más o menos con la siguiente pinta:
public class MyCustomMiddleware
{
    private readonly RequestDelegate _next;
    public MyCustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task Invoke(HttpContext context)
    {
        // Hacer algo antes de pasar el control al siguiente middleware
        await _next(context); // Pasar el control al siguiente middleware
        // Hacer algo después de ejecutar el siguiente middleware
    }
}
Estas clases no heredan de ninguna otra ni implementan interfaces proporcionadas por el framework, aunque atienden a convenciones simples, como la recepción en el constructor del delegado al siguiente middleware en el pipeline, o la existencia de un método Invoke() o InvokeAsync(), que es donde introduciremos nuestra lógica, recibiendo el contexto HTTP.

La ausencia de interfaces o clases base aporta flexibilidad, pero elimina las ayudas propias del tipado fuerte y puede ser fuente de problemas si no atendemos a estas convenciones con cuidado. Es decir, si en lugar del método Invoke() por error escribimos Invke(), nada fallará en compilación. Tendremos que esperar a ejecutar la aplicación para que explote.

También es importante tener en cuenta que una clase middleware sólo es instanciada una vez, cuando la aplicación está arrancando; luego, en cada petición será ejecutado su método Invoke() sobre la misma instancia, lo que es a priori muy poco intuitivo y puede causarnos algún dolor de cabeza si no somos cuidadosos.

Por ejemplo, en el constructor no podemos recibir dependencias con un lifetime per request, como podría ser un contexto de Entity Framework, porque en el momento de creación de la instancia ni siquiera se han recibido peticiones todavía. Es decir, el siguiente código fallará al intentar inyectar un componente registrado como scoped:
public class AuditMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMyDataContext _dataContext;
    public AuditMiddleware(RequestDelegate next, IMyDataContext dataContext) // Fallará
    {
        _next = next;
        _dataContext = dataContext;
    }
 
    public async Task Invoke(HttpContext httpContext)
    {
        _dataContext.Auditing.Add(
            new AuditEntry() { Description = "New request", Path = httpContext.Request.Path }
        );
        await _dataContext.SaveChangesAsync();
        await _next(httpContext);
    }
}
De hecho, las dependencias scoped deben añadirse como parámetros al método Invoke(). El siguiente código sí funcionaría correctamente, porque la referencia al contexto de datos IMyDataContext se recibe en el interior de un scope delimitado por la petición actual:
public class AuditMiddleware
{
    private readonly RequestDelegate _next;
    public AuditMiddleware(RequestDelegate next) 
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMyDataContext dataContext)
    {
        dataContext.Auditing.Add(
            new AuditEntry() { Description = "New request", Path = httpContext.Request.Path }
        );
        await dataContext.SaveChangesAsync();
        await _next(httpContext);
    }
}

La interfaz IMiddleware

ASP.NET Core proporciona una fórmula para mejorar un poco la forma de crear middlewares: la interfaz IMiddleware. Esta se encuentra definida en el espacio de nombres Microsoft.AspNetCore.Http de la siguiente forma:
public interface IMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}
Las clases middleware que implementen IMiddleware presentan dos diferencias principales respecto a las "clásicas":
  • Primero, el uso del tipado fuerte nos evitará fallos en tiempo de ejecución, puesto que estaremos obligados a implementar el método InvokeAsync(), que es desde donde se procesarán las peticiones. Fijaos además que se recibe tanto el contexto HTTP como el delegado que nos permitirá ceder el control al siguiente middleware del pipeline.
     
  • Segundo, las instancias de este middleware serán solicitadas al contenedor de servicios de ASP.NET Core en cada petición a través de una factoría. Es decir, no se trata de una instancia única como teníamos en el caso anterior. Como consecuencia:

    • Debemos registrar obligatoriamente el middleware en el método ConfigureServices() de la clase Startup para que esté disponible. Si no es así, la aplicación explotará cuando llegue la primera petición porque el framework no será capaz de crear la instancia.
       
    • Su ciclo de vida será el que especifiquemos en el momento de registrarlas. Lo habitual será hacerlo como transient, pero podríamos elegir otra estrategia si es necesario.
       
    • Dado que es el propio framework el que creará las instancias, podemos utilizar inyección de dependencias en el constructor.
Por tanto, un middleware como el anterior podríamos reescribirlo usando IMiddleware de la siguiente forma:
public class AuditMiddleware: IMiddleware
{
    private readonly IMyDataContext _dataContext;
    public AuditMiddleware(IMyDataContext context) 
    {
        _dataContext = context;
    }
 
    public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next)
    {
        _dataContext.Auditing.Add(
            new AuditEntry() { Description = "New request", Path = httpContext.Request.Path }
        );
        await _dataContext.SaveChangesAsync();
        await next(httpContext);
    }
}
Y para registrarlo y añadirlo al pipeline, lo haríamos de forma similar a lo habitual, por ejemplo:
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddTransient<AuditMiddleware>();
    ...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    app.UseMiddleware<AuditMiddleware>();
    ...
}
Como nota negativa, decir que los middlewares que implementan IMiddleware no permiten el envío de parámetros de inicialización personalizados. Es decir, un código como el siguiente fallará en tiempo de ejecución:
app.UseMiddleware<MyMiddleware>(new MyMiddlewareOptions { ... });
En definitiva, se trata de una fórmula adicional, denominada oficialmente factory-based middlewares, para implementar middlewares que soluciona algunos problemas propios del enfoque basado en convenciones. Aunque de momento limitado por la imposibilidad de enviar parámetros de inicialización, sin duda es una buena opción a tener en cuenta en casos en los que esto no es necesario.

Publicado en: www.variablenotfound.com.

Aún no hay comentarios, ¡sé el primero!