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, 26 de abril de 2016
ASP.NET Core
Al hilo de lo tratado en el último post sobre la ramificación del pipeline de ASP.NET Core utilizando los extensores Map() y MapWhen(), el amigo Fernando V. dejaba una pregunta interesante en los comentarios del blog:
"[…] Igual que hemos visto cómo ramificar el pipeline, sería posible volver a unir las distintas ramas en una sola?"
Tampoco creo que sea muy habitual crear este tipo de estructuras en el pipeline, pero la verdad es que me ha parecido un reto muy interesante, lo suficiente como para dedicarle un post ;D

Definición de objetivos

Pipeline con distintos recorridosEl objetivo es configurar el pipeline de forma que la ejecución pueda desviarse a una rama determinada, pero que al finalizar ésta vuelva a la rama principal.

Seguro que lo entendéis mejor si echáis un vistazo al diagrama lateral, donde representamos una estructura en la que:
  • Existe una rama principal del pipeline, en la que posicionamos un middleware llamado "Middleware 1".
  • A continuación introducimos una rama para las peticiones cuya ruta que comience por "/test", en la que introducimos un middleware llamado "Middleware 2.2".
  • Mientras en la rama principal, fuera de esta ramificación que hemos creado, introducimos el "Middleware 2.1".
  • Por último, ambas ramas confluyen en un punto común, en el que hemos posicionado el "Middleware 3".
De esta forma, el proceso de una petición a la ruta "/" sería procesada secuencialmente por los middlewares "1", "2.1" y "3", mientras que una petición a "/test" ejecutaría los middlewares "1", "2.2" y "3".

¿Interesante, eh? ;D

Tras reflexionar un poco, veo que el problema podría solucionarse al menos de dos formas, ambas igualmente divertidas ;D y que, en cualquier caso, ayudan a conocer mejor cómo funciona todo esto por dentro:
  1. Creando un middleware que permita configurar pipelines alternativos, similar a los extensores Map() o MapWhen() que hemos visto en otro post.
  2. Troceando el pipeline en varias secciones que engancharemos entre si a nuestra conveniencia.
Vamos a ver ambas implementaciones, pero antes permitidme un inciso…

Un middleware que deja rastro: TrackMiddleware

Vamos a echar un ojo rápidamente al middleware que vamos a utilizar para probar nuestras implementaciones, un componente que simplemente retornará al cliente la cadena que le indiquemos y continuará ejecutando el pipeline. Si esto de programar middlewares y lo que ves a continuación te suena a arameo antiguo, quizás deberías echar un vistazo al post Custom middlewares en ASP.NET Core que publicamos por aquí hace algún tiempo, donde explicaba cómo crear middlewares personalizados.

Bien, pues el código del middleware es el siguiente:
public class TrackMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _text;

    public TrackMiddleware(RequestDelegate next, string text)
    {
        _next = next;
        _text = text;
    }

    public async Task Invoke(HttpContext ctx)
    {
        await ctx.Response
                 .WriteAsync($"{_text} in path {ctx.Request.Path}\n");
        await _next(ctx);
    }
}
Como podéis observar, simplemente escribimos por el canal de salida el mensaje especificado, y pasamos el control al siguiente middleware. Así de sencillo.

Ahora veamos el clásico extensor para facilitar la inserción del middleware anterior en el pipeline:
public static class TrackMiddlewareExtensions
{
    public static IApplicationBuilder UseTracking(this IApplicationBuilder app, 
                                                  string text)
    {
        return app.UseMiddleware<TrackMiddleware>(text);
    }
}
De esta forma, un ejemplo de uso podría ser:
public void Configure(IApplicationBuilder app)
{
    app.UseTracking("Start of pipeline");
    ...
    app.UseTracking("End of pipeline");
}
Pipelines alternativosY dicho todo esto, vamos al tema objeto del post…

Solución 1: Pipelines alternativos con IfMiddleware

La idea aquí consiste en implementar un middleware similar a MapWhen() que hemos visto en otros posts, pero que admita la configuración de dos pipelines, uno para cuando el predicado sea cierto y otro para cuando sea falso. Gráficamente, lo que vamos a montar es algo similar lo que vemos en el diagrama adjunto:
  • Tendremos el pipeline principal, en el que insertaremos el middleware "1".
  • A continuación, sobre el mismo pipeline principal, colocaremos el middleware IfMiddleware cuya codificación vamos a ver más adelante. En él configuraremos la condición que queremos evaluar, y los dos branches que ejecutarán las peticiones cuando dicha condición sea cierta o falsa, respectivamente.
  • En el branch que se ejecutará cuando la condición sea cierta, añadiremos el middleware "2.2". Para el caso contrario, insertaremos el middleware "2.1" en el otro branch.
  • Finalmente, de vuelta en el pipeline principal, insertaremos el middleware "3".
Comenzando por el final, a nivel de código lo configuraremos de la siguiente forma en la clase de inicialización de la aplicación:
public void Configure(IApplicationBuilder app)
{
    app.UseTracking("Middleware 1");
    app.UseIf(
        ctx => ctx.Request.Path.StartsWithSegments("/test"),
        trueBranch =>
        {
            trueBranch.UseTracking("Middleware 2.2");
        },
        falseBranch =>
        {
            falseBranch.UseTracking("Middleware 2.1");
        });
    app.UseTracking("Middleware 3");
}
Como podemos observar, en el primer parámetro de UseIf() indicamos la condición que queremos que se cumpla; a continuación, el delegado que configura el pipeline para cuando la condición evalúe a cierto, y seguidamente el delegado para cuando la condición sea falsa.

Así, una petición hacia el raíz retornará los mensajes emitidos por los middlewares "1", "2.1" y "3", mientras que si la ruta de la petición fuera "/test" se retornaría "1", "2.2" y "3".

El código del middleware sería el mostrado a continuación (ojo, he omitido comprobaciones de nulos o valores inválidos en parámetros para abreviar):
public class IfMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IfOptions _options;

    public IfMiddleware(RequestDelegate next, IfOptions options)
    {
        _next = next;
        _options = options;
    }

    public async Task Invoke(HttpContext context)
    {
        if (this._options.Predicate(context)) {
            await this._options.TrueBranch(context);
        }
        else { 
            await this._options.FalseBranch(context);
        }
        await this._next(context);
    }
}
Y por último, veamos cómo implementar el extensor UseIf() que hemos utilizado más arriba para incluir IfMiddleware en el pipeline y configurarlo apropiadamente:
public static class IfExtensions
{
    public static IApplicationBuilder UseIf(
                this IApplicationBuilder app, 
                Func<HttpContext, bool> predicate,
                Action<IApplicationBuilder> trueConfig, 
                Action<IApplicationBuilder> falseConfig)
    {
        // Create and configure the "true" branch
        IApplicationBuilder trueAppBuilder = app.New();
        trueConfig(trueAppBuilder);

        // Create and configure the "false" branch
        IApplicationBuilder falseAppBuilder = app.New();
        falseConfig(falseAppBuilder);

        // Add middleware to the pipeline
        var options = new IfOptions()
        {
            Predicate = predicate,
            TrueBranch = trueAppBuilder.Build(),
            FalseBranch = falseAppBuilder.Build()
        };
        return app.UseMiddleware<IfMiddleware>(options);
    }
}
Con esto habríamos conseguido nuestro objetivo. Quizás parezca algo aparatoso, pero hemos implementado una solución genérica para crear ramas alternativas en el pipeline, que quizás en algún momento pueda resultarnos útil.

Pipeline por tramosSolución 2: troceando el pipeline

Con la primera opción que hemos visto ya podríamos solucionar el problema que nos planteábamos en un escenario real, pero vamos a ver esta segunda opción para seguir aprendiendo sobre el funcionamiento del pipeline de ASP.NET Core :)

La idea en esta ocasión será "trocear" el pipeline de la aplicación en distintas porciones, y empalmarlas en los puntos que nos interese para conseguir nuestro objetivo. En el diagrama adjunto podéis ver que tendremos tres pipelines distintos:
  1. El pipeline principal, donde posicionaremos el middleware "1" y el "2.1".
  2. El pipeline de la rama que se activará sólo cuando se realicen peticiones a "/test", donde introduciremos el middleware "2.2".
  3. El pipeline común, en el que insertaremos el middleware "3" y a donde debe llegar la ejecución independientemente del camino tomado.
En este caso, dado que no necesitamos middlewares ni otros componentes que no vengan de serie en el framework, vamos a implementación la solución completa desde el método Configure() de la clase de inicialización:
public void Configure(IApplicationBuilder app)
{
    // Create and configure the common pipeline (the last segment)
    var common = app.New();
    common.UseTracking("Middleware 3");
    var commonPipeline = common.Build();
    
    // Configure the main (default) pipeline
    app.UseTracking("Middleware 1");
    app.Map("/test", branch =>
    {
        // Configure the branch
        branch.UseTracking("Middleware 2.2");
        branch.Run(commonPipeline);     // Join the "common" pipeline
    });

    // Continue configuration of main pipeline
    app.UseTracking("Middleware 2.1");
    app.Run(commonPipeline);           // Join the "common" pipeline
}
Lo que hacemos es lo siguiente:
  • En primer lugar creamos y configuramos el último tramo del pipeline (azul en el diagrama), al que llamamos commonPipeline.
  • A continuación configuramos el pipeline principal (en verde):
    • Introducimos el middleware "1",
    • Seguidamente añadimos MapMiddleware para crear el branch condicional (en rojo):
      • le añadimos el middleware "2.2"…
      • y lo "enganchamos" con commonPipeline.
    • Volviendo al pipeline principal, le añadimos ahora el middleware "2.1".
    • Lo "enganchamos" a commonPipeline.
Observad que cuando hablamos de "enganchar" un pipeline con otro, en la práctica no es más que hacer que el primer pipeline acabe mediante una invocación al segundo, utilizando una sobrecarga del extensor Run() de IApplicationBuilder.

¡Y esto, es todo! De nuevo tendríamos solucionado el problema que proponíamos al principio del post con una solución más sencilla de implementar, aunque menos potente y elegante que la primera que vimos.

Como decía anteriormente, seguro que lo que hemos visto no nos va a ser de utilidad todos los días, pero está bien saber que existen estas posibilidades y, en cualquier caso, ha sido un ejercicio interesante para comprender mejor cómo funciona el pipeline y conocer la flexibilidad de su diseño.

Y, por supuesto, agradecer a Fernando y muchos otros que, como él, siguen haciendo de este espacio un lugar donde aprender juntos.

Publicado en Variable not found.

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