lunes, 29 de agosto de 2016

Asp.Net Core: el pipeline de peticiones de la aplicación

Anteriormente en NoCompila vimos como se hacía un Hola Mundo muy sencillo en Asp.Net Core.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
           var host = new WebHostBuilder()
            .UseKestrel()
            .Configure(app => {
                app.Run(async (context)=>await context.Response.WriteAsync("Hola"));
            })
            .Build();
            host.Run();
        }
    }
}

En Asp.Net Core se entiende como middleware un delegado que recibe por parámetro un httpContext que permite manejar la petición y la respuesta web. Cada middleware decide si se debe invocar al siguiente delegado, pudiendo ejecutar código antes y después de la invocación.
El pipeline de peticiones debería estar compuesto por al menos un middleware.

Middelware03

En el ejemplo de Hola Mundo, vemos como en la línea 14 es donde se establece el pipeline de peticiones de la aplicación (no se me ocurre ninguna traducción que suene bien para este termino), es decir donde se añade al pipeline de peticiones de la aplicación el único middleware usando el método Run.
Para construir el pipeline tenemos tres métodos:

  • Run: Añade el último delegado a ejecutar en el pipeline, si solo tenemos uno, será este método el que usemos. Es el que hemos usado en el HolaMundo
  • Map y MapWhen: Deciden en base a base a la ruta de la petición web si se ejecuta un middleware
  • Use: Es la manera estándar de añadir un middleware al pipeline.

Los componentes de middleware se ejecutan en el orden en el que se añaden al pipeline.
Microsoft recomienda no modificar el HttpResponse después de hacer la invocación del siguiente middleware.
En el ejemplo que vamos a poner a continuación hacemos caso omiso de las recomendaciones de Microsoft para enseñar el orden de ejecución de los componentes, no es código real de una aplicación de producción:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
           var host = new WebHostBuilder()
            .UseKestrel()
            .Configure(app => {
                app.Use(async (context, next)=>{
                    context.Response.WriteAsync("Inicio Use1 \n");
                    await next.Invoke();
                    context.Response.WriteAsync("\nFin Use1");
                });
                app.Use(async (context, next)=>{
                    context.Response.WriteAsync("Inicio Use2 \n");
                    await next.Invoke();
                    context.Response.WriteAsync("\nFin Use2");
                });
                app.Run(async (context)=>await context.Response.WriteAsync("Hola"));
            })
            .Build();
            host.Run();
        }
    }
}

En el ejemplo vemos como al ejemplo anterior le hemos añadido dos métodos Use. Si ejecutamos ese código veremos que primero se ejecuta la primera línea del primer use, al ejecutar next.Invoke pasa al siguiente middleware y después ejecuta la tercera línea. Es decir que la salida que tendremos en el navegador es:

Inicio Use1 
Inicio Use2 
Web Asombrosa
Fin Use2
Fin Use1

En el código podemos ver como al usar Use tenemos que recordar ejecutar el siguiente middleware con la llamada next.Invoke, a no ser que nos interese interrumpir el flujo. De hecho un Use sin next.Invoke es igual a un Run.

Con Map se permite tomar decisiones sobre qué se ejecuta en base a la ruta de la petición web. El la clase HttpRequest contiene una propiedad Path que contiene la ruta de la petición web. Cuando se ejecuta Map, la parte de la ruta que coincide con el criterio que estamos poniendo se quita de la propiedad y se añade a la propiedad PathBase. Veamos un ejemplo:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
           var host = new WebHostBuilder()
            .UseKestrel()
            .Configure(app => {
                app.Map("/manelmola",(app2)=> app2.Run(
                    async (context)=>await context.Response.WriteAsync(
                        string.Format ("Manel Mola\nPath={0}\nPathBase={1}", 
                            context.Request.Path, 
                            context.Request.PathBase
                        )
                    )
                ));
                app.Run(async (context)=>await context.Response.WriteAsync(
                    string.Format ("Hola\nPath={0}\nPathBase={1}", 
                        context.Request.Path, 
                        context.Request.PathBase
                    )
                ));
            })
            .Build();
            host.Run();
        }
    }
}

En este caso al Usar Map si ponemos la ruta coincide con ManelMola escribe “Manel Mola” en caso contrario escribe Hola. Como se puede ver en las capturas:
MapLocalhostMapBlablaMapManelMola

Usar MapWhen es muy parecido, pero en vez de poner un patrón de ruta, el primer parámetro es un delegado que devuelve un boolean, si el primer parámetro devuelve true, se ejecutará el segundo delegado.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
           var host = new WebHostBuilder()
            .UseKestrel()
            .Configure(app => {
                app.MapWhen(context => context.Request.Query.ContainsKey("mola"),
                    (app2)=> app2.Run(
                        async (context)=>await context.Response.WriteAsync("MOLA")
                ));
                app.Run(async (context)=>await context.Response.WriteAsync("Hola"));
            })
            .Build();
            host.Run();
        }
    }
}

MapWhenNoMolaMapWhenMola

Cuando la lógica de un middleware es compleja Microsoft recomienda que se exponga mediante un método extensor ocultando la complejidad del componente. Vamos a ver un ejemplo sencillo donde programamos un cronómetro para medir la velocidad del resto de ejecuciones del pipeline:

using Microsoft.AspNetCore.Http;
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;

namespace WebApplication{
    public static class StopwatchMiddelwareExtensions{
         public static IApplicationBuilder UseStopWatchMiddelware(this IApplicationBuilder builder){
            builder.Use(async (context, next)=>{
				var cronometro = new Stopwatch();
				cronometro.Start();
				await next.Invoke();
				cronometro.Stop();
				await context.Response.WriteAsync(string.Format("Se ha ejecutado en {0} milisegundos", cronometro.ElapsedMilliseconds));
			});
            return builder;
        }
    }  
}

Al usar el método extensor, el código queda mucho más limpio (línea 16):

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using System;
using System.Threading;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
           var rnd = new Random();
           var host = new WebHostBuilder()
            .UseKestrel()
            .Configure(app => {
                app.UseStopWatchMiddelware();
                app.Run(async (context)=>{
                    Thread.Sleep(rnd.Next(0,1000));
                });
            })
            .Build();
            host.Run();
        }
    }
}

Hay formas más sofisticadas para la creación y encapsulamiento de middlewares, pero como punto introductorio con esto están cubiertos los conceptos básicos. Si buscas un ejemplo más avanzado de como crear encapsular un middleware lo puedes encontrar aquí.

Asp.Net Core trae una serie de middlewares ya programados:

  • Authentication: Da soporte a la autenticación.
  • CORS: Configura el uso cruzado de recursos (cuando se solicita un recurso de un dominio distinto al que se pertenece).
  • Routing: Define en enrutamiento.
  • Session: Permite el uso de sesiones de usuario.
  • Static Files: Ofrece soporte para el uso de ficheros estáticos y exploración de directorios.

No hay comentarios:

Publicar un comentario