Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí
Asp.net core se basa en el concepto de middleware. En este modelo la petición web viaja a través de un conjunto de componentes. Cada componente recibe la petición y puede:
- Modificar la petición y enviarla al siguiente componente
- O bien, generar una respuesta y enviarla de vuelta al componente anterior.
De la misma manera, cuando un componente recibe una respuesta puede modificarla (o no) y enviarla al componente anterior. Cuando la respuesta llega al primer componente, es enviado de vuelta al cliente (navegador):
Esta imagen muestra dos casos. En el primero, la petición pasa por los cuatro componentes. Cada componente puede modificar la petición con lo que considere oportuno antes de pasarla al siguiente componente. La petición llega al último componente que genera una respuesta, que es enviada “para atrás” pasando por todos los componentes, hasta llegar al cliente.
En el segundo caso, el segundo componente ha decidido “cortocircuitar” la petición y ya no la envía al siguiente componente. En su lugar genera una respuesta, y la envía “hacia atrás” hasta que llega de nuevo al cliente.
A nivel de nomenclatura, a cada uno de esos componentes les llamamos middleware y al conjunto de componentes lo llamamos el pipeline de la aplicación.
Ventajas del uso de middlewares
La ventaja principal de los middlewares es que componentizan el framework y permiten reaprovechar muchas partes de él. Supongamos un framework que no use este concepto, tal y como es ASP.NET 4.5 sin usar OWIN. En este caso montamos un framework sobre ASP.NET 4.5, tal y como puede ser MVC5. Este framework debe encargarse de todo, incluyendo la autenticación y la autorización. Ahora alguien quiere montar otro framework distinto, pongamos NancyFx. Pues de nuevo NancyFx debe tener sus mecanismos de autenticación y autorización propios. Harán exactamente lo mismo (validar una cookie) pero son códigos distintos.
¿No sería mucho mejor poder tener un sistema de validación de cookies único? ¿Y que cualquier framework que se montase sobre él pudiese reaprovecharlo? Pues eso es lo que trae el uso de middlewares. En el ejemplo anterior el segundo middleware (el recuadro naranja) podría ser un framework de autenticación, que solo haría eso: validar la cookie. En el caso de no existir, cortocircuita la petición y envía una respuesta de error.
En este modelo, tanto MVC como NancyFx se pueden despreocupar de la autenticación de los usuarios: esa tarea recae en un middleware externo, que se sitúa antes en el pipeline de la aplicación.
Nota: En ASP.NET tradicional, pre-OWIN se pueden crear también componentes parecidos a los middlewares (a través de HttpModules) pero el concepto no es ni tan práctico, ni tan sencillo como el concepto de middleware en OWIN o en ASP.NET Core.
Esa idea apareció en ASP.NET de la mano de OWIN y en ASP.NET Core se ha mantenido. Aunque los middlewares ASP.NET Core no son middlewares OWIN, la idea es la misma.
¿Qué es un middleware ASP.NET Core?
Hemos visto, a nivel conceptual, qué es un middleware. Veamos ahora en detalle qué es un middleware ASP.NET Core. Pues en el fondo un middleware ASP.NET Core se puede conceptualizar como un par de funciones:
- f(req) = req’
- f(res) = res’
Es decir, una función toma una petición y genera otra petición (igual o modificada) y otra toma una respuesta y genera otra respuesta (igual o modificada). Viendo esto uno podría pensar que existe una interfaz tipo IMiddleware o algo así, pero no. Se usa más un sistema basado en convenciones. Veamos como definir un middleware en C#:
- public class CustomMiddleware
- {
- private readonly RequestDelegate _next;
- public CustomMiddleware(RequestDelegate next)
- {
- _next = next;
- }
- public async Task Invoke(HttpContext context)
- {
- await _next.Invoke(context);
- }
- }
Esto define un middleware ASP.NET Core. Observa que la clase no hereda de ninguna clase, ni implementa interfaz alguna. Las convenciones son:
- Que exista un constructor que acepte un RequestDelegate
- Que exista un método Invoke que acepte un HttpContext
Observa que es nuestra responsabilidad llamar al siguiente middleware. Eso es lo que nos permite cortocircuitar la petición (simplemente no lo llamamos y listo).
Nota: De hecho es posible incluso tener un middleware que no sea ni una clase. A nivel del framework un middleware es realmente una Func<RequestDelegate, RequestDelegate> o una Func<HttpContext, Func
Este middleware no hace nada, ni modifica la respuesta. Pero ya podríamos hacer lo que quisieramos. Veamos un ejemplo: si la petición contiene una cabecera X-User vamos a autenticar la petición con este usuario:
- public async Task Invoke(HttpContext context)
- {
- if (context.Request.Headers.Keys.Contains(“X-User”))
- {
- var user = context.Request.Headers[“X-User”].ToString();
- var claim = new Claim(ClaimTypes.Name, user);
- var principal = new ClaimsPrincipal(new ClaimsIdentity(new[] { claim }, “xuser”));
- context.User = principal;
- }
- await _next.Invoke(context);
- }
Nos falta una cosa, que es como añadir nuestro middleware al pipeline de la aplicación. Para ello basta con usar el método UseMiddleware
- public void Configure(IApplicationBuilder app)
- {
- app.UseIISPlatformHandler();
- app.UseMiddleware<CustomMiddleware>();
- app.UseMvcWithDefaultRoute();
- }
En este ejemplo tengo también MVC6 instalado en el pipeline (MVC6 es simplemente otro middleware) y ahora puedo tener un controlador como el que sigue:
- public class HomeController : Controller
- {
- [Authorize]
- public IActionResult Index()
- {
- return Ok(new { user = User.Identity.Name });
- }
- }
Observa que la acción Index está protegida por [Authorize]. Pero [Authorize] lo único que hace es validar que haya una identidad autenticada en el contexto… que es precisamente lo que ha dejado nuestro CustomMiddleware. El resultado, ya te lo puedes imaginar, es que si realizamos una petición con X-User se puede acceder a la acción, como muestra esta captura de Postman:
Si no envías la cabecera X-User, entonces recibes el 401, que es lo que envía Authorize, por defecto:
Hemos visto como nuestro middleware puede modificar la petición (o el contexto) antes de mandarlo al siguiente middleware (que en nuestro caso es MVC6). Pero hagamos otra cosa. Veamos como podemos modificar la respuesta antes de enviarla al cliente.
Para ello, cambiaremos el StatusCode. Si es 401, mandaremos un 500. El método Invoke ahora queda:
- public async Task Invoke(HttpContext context)
- {
- if (context.Request.Headers.Keys.Contains(“X-User”))
- {
- var user = context.Request.Headers[“X-User”].ToString();
- var claim = new Claim(ClaimTypes.Name, user);
- var principal = new ClaimsPrincipal(new ClaimsIdentity(new[] { claim }, “xuser”));
- context.User = principal;
- }
- await _next.Invoke(context);
- if (context.Response.StatusCode == 401)
- {
- context.Response.StatusCode = 500;
- }
- }
Observa el patrón: lo que va antes del await _next.Invoke permite modificar el contexto o la petición antes de mandarla al siguiente middleware. Y lo que va después del await, permite modificar la respuesta antes de mandarla para atrás al middleware anterior.
Si ahora ejecutas de nuevo la petición con postman, y no le pasas el X-User verás que ahora recibes un 500:
Nuestro middleware transforma el 401 que devuelve MVC6 en un 500 antes de mandarlo al cliente.
Por supuesto, cuando uno crea un middleware suele crear un método de extensión sobre IApplicationBuilder que permita usarlo de forma más sencilla:
- namespace Microsoft.AspNet.Builder
- {
- public static class CustomMiddlewareAppBuilderExtensions
- {
- public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
- {
- return app.UseMiddleware<CustomMiddleware>();
- }
- }
- }
Y así nuestro método Startup queda “más profesional” 😛
- public void Configure(IApplicationBuilder app)
- {
- app.UseIISPlatformHandler();
- app.UseCustomMiddleware();
- app.UseMvcWithDefaultRoute();
- }
Bueno… vamos a dejarlo aquí en este post. Espero que haya quedado un poco más claro el concepto de middleware en ASP.NET Core y ¡lo sencillo que es crear uno!