martes, 15 de diciembre de 2015
El 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étodoConfigure()
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:
- Se hace algo antes de que se ejecuten los middlewares posicionados posteriormente en el pipeline.
- Se cede el control al siguiente middleware del pipeline.
- Se hace algo después de que los middlewares posteriores se hayan ejecutado.
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.
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étodoConfigure()
, 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()
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 } } }
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!
Enviar un nuevo comentario