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, 15 de diciembre de 2015

ASP.NET CoreEl concepto de proceso de peticiones basado en el pipeline no es algo nuevo, pero ciertamente es en ASP.NET Core donde se hace más explícito y visible a los desarrolladores.

Y aunque anteriormente también hemos trabajado con middlewares (en ASP.NET 4.x los módulos y handlers podían ejercer funciones similares, y más recientemente, en OWIN ya existía el mismo concepto), es ahora cuando debemos conocerlos bien si queremos llegar a comprender y dominar la nueva plataforma ASP.NET Core.

Este este post vamos a profundizar un poco en el proceso de peticiones en ASP.NET Core, y veremos lo sencillo que resulta crear middlewares personalizados que participen en dicho proceso.

Cómo se procesa una petición en ASP.NET Core

Cuando una petición llega a una aplicación ASP.NET Core, es procesada por los middlewares que han sido introducidos en el pipeline desde el método Configure() de la clase Startup, y que componen una cadena colaborativa de proceso de peticiones.

En general, el esquema de funcionamiento habitual en un middleware es el siguiente:
  1. Se hace algo antes de que se ejecuten los middlewares posicionados posteriormente en el pipeline.
  2. Se cede el control al siguiente middleware del pipeline.
  3. Se hace algo después de que los middlewares posteriores se hayan ejecutado.
Obviamente, un middleware puede necesitar implementar estos tres pasos, o sólo alguno de ellos. Por ejemplo, un sistema de logging de peticiones podría simplemente registrar una traza con la petición recibida y seguidamente pasar el control al siguiente middleware (pasos 1 y 2), puesto que no tendría que hacer nada más cuando éste acabara su ejecución. En cambio, si estuviésemos registrando información de las respuestas, este middleware pasaría el control al siguiente, y tras su ejecución es cuando registraría la traza (pasos 2 y 3).

El siguiente diagrama muestra un pipeline con cuatro middlewares y cómo los primeros van tomando el control de forma sucesiva y lo van pasando al siguiente hasta llegar al tercero de ellos, que decide procesar completamente la petición y retornar la respuesta. En este caso, el cuarto middleware ni siquiera se enteraría de que una petición entró en el pipeline y fue procesada:


Internamente esta cadena de middlewares se implementa en forma de lista enlazada simple, donde cada middleware necesita tener una referencia hacia el siguiente para poder pasarle el control, y hacerlo de forma expresa (de ahí que antes hablara de una cadena colaborativa).

Custom middlewares básicos

La forma más sencilla de añadir un middleware personalizado al pipeline es desde el mismo método Configure() de la clase Startup. Ahí mismo podemos usar el extensor Use() de IApplicationBuilder y suministrar el middleware en forma de delegado con el código que aportará su granito de arena en el proceso de la petición. Su esquema podría ser el siguiente:
public void Configure(IApplicationBuilder app)
{
    ... // Otros middlewares
    app.Use(async (context, next) =>
    {
        // Hacer algo antes de pasar el control al siguiente middleware
        await next(); // Pasar el control al siguiente middleware
        // Hacer algo después de ejecutar el siguiente middleware
    });
    ... // Otros middlewares
}
El delegado, expresado en forma de lamda asíncrona, recibe como parámetros el contexto de la petición, desde donde podemos acceder a toda la información relacionada con la misma (request, response, etc.), y un delegado que podemos invocar para que el proceso de la petición continúe su periplo por el pipeline.

Por ejemplo, el siguiente middleware es un profiler muy simple. Inicia un cronómetro antes de pasar el control al siguiente middleware, y tras esperar a que finalice su ejecución, envía al cliente el tiempo transcurrido. 
app.Use(async (context, next) =>
{
    var watch = Stopwatch.StartNew();
    await next();
    await context.Response.WriteAsync(
            "-- Elapsed: " + watch.Elapsed
    );
});
En la captura de pantalla de la izquierda podemos ver el resultado si añadimos este middleware a la applicación creada usando la plantilla de proyecto MVC 6.

Obviamente, podemos usar esta fórmula para escribir cualquier tipo de middleware, pero si queremos hacer algo más estándar y reutilizable es mejor optar por el modelo utilizado por los propios middlewares de ASP.NET.

Custom middlewares like a boss

Para cuando la cosa se complica y es demasiado código como para incluirlo inline en el método  Configure(), o simplemente queremos mejorar su reutilización y facilitar su distribución, el enfoque más idóneo es llevarse el código del middleware a una clase especializada.

En este caso, una plantilla que podría utilizarse es la mostrada a continuación:

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
    }
}
En este caso, para añadir el middleware en el pipeline usaríamos el siguiente código en el método Configure() de la clase Startup:
app.UseMiddleware<mycustommiddleware>();
Como se podía intuir, el parámetro que recibe el constructor es el delegado al siguiente middleware presente en el pipeline y es suministrado de forma automática por el framework, pero, ¿y si necesitásemos parámetros adicionales en el constructor, por ejemplo opciones de configuración?

En este caso, simplemente añadiríamos los parámetros al constructor y se los suministraríamos en el momento de añadirlo al pipeline:
public class MyCustomMiddleware
{
    public MyCustomMiddleware(RequestDelegate next, MyCustomOptions options)
    {
        _next = next;
        _options = options;
    }
    ... // Omitido
}

// En Configure():
app.UseMiddleware<MyCustoMiddleware>(new MyCustomOptions() { ... });
Veamos un ejemplo algo más real. Retomemos el mini-profiler que vimos anteriormente, y vamos a transformarlo en una clase middleware a la que, además, podremos suministrar el formato en el que mostraremos el tiempo transcurrido:
public class SimpleProfilerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _format;

    public SimpleProfilerMiddleware(RequestDelegate next, 
                                    string format = "-- Elapsed: {0}")
    {
        _next = next;
        _format = format;
    }

    public async Task Invoke(HttpContext context)
    {
        var stopWatch = Stopwatch.StartNew();
        await _next(context);
        await context.Response.WriteAsync(string.Format(_format, stopWatch.Elapsed));
    }
}
Y en este caso podemos añadirlo al pipeline de la siguiente forma:
app.UseMiddleware<MyCustomMiddleware>("Tiempo: {0}");
... // Otros middlewares

Facilitar la inclusión en el pipeline: métodos UseXXX()

Pero seguro que os habéis fijado en que los middlewares suministrados por ASP.NET y otras soluciones usan una fórmula mucho más sencilla y atractiva para añadir middlewares al pipeline: los métodos UseXXXX(), como  UseStaticFiles() o  UseRuntimeInfoPage().

Si queremos llegar hasta este punto, lo único que tenemos que hacer es crear nuestros propios extensores sobre IApplicationBuilder, poco más o menos de la siguiente forma:
namespace Microsoft.AspNet.Builder
{
    public static class SimpleProfilerMiddlewareExtensions
    {
        public static IApplicationBuilder UseSimpleProfiler(
                          this IApplicationBuilder app, string format)
        {
            app.UseMiddleware<SimpleProfilerMiddleware>(format);
            return app; // To allow method chaining
        }
    }
}
Intellisense
Primero, fijaos que lo definimos en el espacio de nombres Microsoft.AspNet.Builder para que podamos descubrir fácilmente este extensor con intellisense desde la clase de inicialización de la aplicación.

Por otra parte, el retorno de la instancia de  IApplicationBuilder es simplemente para permitir el encadenamiento de llamadas, al más puro estilo "fluent API", por seguir la convención usados por otros componentes de este tipo.

Hecho esto, ya podemos añadir nuestro middleware al pipeline como mandan los cánones:
app.UseSimpleProfiler("Tiempo: {0}");
... // Otros middlewares

Publicado en: www.variablenotfound.com.

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