0

Short-circuit routing En .NET 8

En este artículo, profundizó en una característica novedosa incorporada a ASP.NET Core en .NET 8: el enrutamiento de cortocircuito. Exploró las distinciones entre esta modalidad y el enrutamiento convencional, destacando las razones fundamentales para considerar su aplicación y detallando su implementación dentro del marco.

El enrutamiento de cortocircuito se erige como una herramienta dinámica, aportando flexibilidad y agilidad al proceso de definir rutas en nuestras aplicaciones ASP.NET Core. Desentraño las diferencias clave entre esta metodología y el enrutamiento tradicional, proporcionando una visión clara de cómo esta funcionalidad puede optimizar la navegación dentro de nuestras aplicaciones web.

Cómo se lleva a cabo el enrutamiento de puntos de acceso en ASP.NET Core

Se destacan ciertos conceptos esenciales en ASP.NET Core, como:

  • Configuración
  • Inserción de dependencias
  • Middleware (Middleware)
  • Enrutamiento

Cada uno de estos conceptos involucra aspectos más simples y más complejos, pero considero que la interacción entre el enrutamiento y el middleware suele ser el desafío principal para las personas. Específicamente, el hecho de que el enrutamiento en ASP.NET Core sea gestionado por dos componentes de middleware.

El Middleware EndpointRouting determina qué punto final registrado se ejecutará en tiempo de ejecución para una solicitud específica, a veces referido como (el término utilizado en este artículo) RoutingMiddleware. Por otro lado, el Middleware EndpointMiddleware se coloca comúnmente al final de la cadena de middleware. Este middleware ejecuta el punto de conexión seleccionado por el para una solicitud específica, gestionado por el Middleware RoutingMiddleware.

Quizás se esté preguntando por qué se han incorporado dos componentes de middleware en lugar de uno solo. La separación entre la selección de un punto de conexión y su ejecución brinda una ventaja específica: la capacidad de ejecutar middleware entre estos dos eventos. Este middleware puede ajustar su comportamiento según los metadatos asociados al punto de conexión que está por ejecutarse, antes de que se inicie la ejecución del mismo.

Diversas características dependen de este comportamiento para operar correctamente. Dos ejemplos habituales son AuthorizationMiddleware y CorsMiddleware, los cuales deben ser colocados entre RoutingMiddleware y EndpointMiddleware para definir las directrices aplicables al punto de conexión seleccionado.

En términos generales, esta es la dinámica habitual del enrutamiento, ya sea que utilice API mínimas o controladores MVC, y se destaca por su alta capacidad de adaptación. Puede enriquecer los puntos de conexión con metadatos adicionales y, posteriormente, incorporar middleware adicional entre el RoutingMiddleware y el EndpointMiddleware para habilitar un comportamiento novedoso.

En ocasiones particulares, un punto de conexión prescinde de la necesidad de autorización, compatibilidad con CORS o la perspectiva de extensión. En estos momentos, entra en acción el enrutamiento por cortocircuito.

¿Qué es el enrutamiento por cortocircuito?

La funcionalidad de enrutamiento de cortocircuito es una incorporación reciente en .NET 8. Se aplica a uno o varios puntos de conexión en la aplicación, lo que implica que el punto de conexión ‘ignora’ conceptualmente el middleware situado entre el RoutingMiddleware y el EndpointMiddleware. Puede concebir esta característica como ‘ignorar’ el middleware entre el RoutingMiddleware y el EndpointMiddleware, o puede interpretarlo como la ejecución inmediata del RoutingMiddleware en el punto de conexión y la omisión del resto de la canalización. En cuanto a la implementación, ASP.NET Core sigue esta última estrategia.

Puede marcar un punto de conexión para usar el enrutamiento ShortCircuit de cortocircuito llamando al punto de conexión en API mínimas.

app.MapGet("/", () => "Hello World!")
   .ShortCircuit();

Al incluir los metadatos de cortocircuito en el punto de conexión, se establece que el punto de conexión ‘¡Hola mundo!’ se ejecutará en el en lugar de hacerlo en el RoutingMiddleware en lugar de EndpointMiddleware. Si lo desea, también puede proporcionar opcionalmente un código de estado que será automáticamente asignado a la respuesta.

app.MapGet("/", () => "Hello World!")
   .ShortCircuit(201)

¿Cuándo es útil?

Entonces, es probable que te estés preguntando en qué situaciones sería útil esto. El diseño del enrutamiento de puntos de conexión se configura de esta manera por una razón, y es porque resulta altamente práctico. Cuando utilizas el enrutamiento de cortocircuito, no puedes aplicar CORS ni autorización al punto de conexión (ni a otras características similares). Creo que el principal caso de uso consiste en reducir la carga de solicitudes que sabes que devolverán una respuesta 404 o que nunca necesitarán autorización o CORS. Algunos ejemplos podrían ser URL conocidas que los navegadores solicitan automáticamente u otras rutas estándar. Por ejemplo:

  • /robots.txt—le dice a los web-scrapers como Google qué indexar.
  • /favicon.ico: el icono de pestaña en un navegador, que el navegador solicita automáticamente.
  • /.well-known/* (todas las rutas con el prefijo ): se utiliza en varias especificaciones, como OpenID Connect, security.txt o webfinger..well-known/

Hablamos de URL sencillas y conocidas que el navegador y otros sitios pueden solicitar automáticamente. Si no las integras en tu sitio, cada solicitud de estas atravesará todo tu middleware y, al final, devolverá un archivo . Eso implica un esfuerzo adicional que simplemente no es requerido. Y con cada navegador solicitando estos archivos, eso podría acumularse. Las rutas de cortocircuito ofrecen la posibilidad de evitar esa sobrecarga. Ahora, llevemos los ejemplos anteriores a una aplicación real:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

         // return a 404 for favicon.ico
app.MapGet("/favicon.ico", () => Task.CompletedTask).ShortCircuit(404);
app.MapGet("/robots.txt", // return a valid robots.txt
    () => """
          User-agent: *
          Allow: /
          """).ShortCircuit(200);

// any request starting with /.well-known/ returns a 404
app.MapShortCircuit(404, ".well-known"); 

app.UseRouting(); // Not required (as added by default) but being explicit in this example

// Any request NOT short-circuited will execute this "middleware", 
// which always throws an exception.
app.Use((HttpContext _, RequestDelegate _)
    => throw new Exception("You shall not pass!"));

// This "normal" endpoint won't ever be executed
app.MapGet("/", () => "Can't ever get to this");
app.Run();

Al ejecutar esta aplicación, conseguirá los resultados previstos:

  • Una solicitud ./favicon.ico devuelve una respuesta 404.
  • Una solicitud /robots.txt devuelve una respuesta 200 con un cuerpo de robots.txt válido. 
  • Cualquier solicitud que comience con devuelve .well-known una respuesta  404.

El resto de las solicitudes sigue el trayecto de la canalización de middleware después de que RoutingMiddleware alcanza el middleware de excepción personalizado y la canalización concluye:

Veamos la implementación

Básicamente, la implementación consta de tres partes:

  • Método ShortCircuit() de extensión que se agrega ShortCircuitMetadata() a un punto de conexión.
  • Método MapShortCircuit() de extensión que agrega un punto de conexión de prefijo de ruta de cortocircuito.
  • Los cambios en el RoutingMiddleware que se comprueban los metadatos y se ejecuta el punto de conexión si es necesario. 
using Microsoft.AspNetCore.Routing.ShortCircuit;

namespace Microsoft.AspNetCore.Builder;

public static class RouteShortCircuitEndpointConventionBuilderExtensions
{
    // These fields cache some common status code values
    private static readonly ShortCircuitMetadata _200ShortCircuitMetadata = new(200);
    private static readonly ShortCircuitMetadata _401ShortCircuitMetadata = new(401);
    private static readonly ShortCircuitMetadata _404ShortCircuitMetadata = new(404);
    private static readonly ShortCircuitMetadata _nullShortCircuitMetadata = new(null);

    public static IEndpointConventionBuilder ShortCircuit(
        this IEndpointConventionBuilder builder, int? statusCode = null)
    {
        var metadata = statusCode switch
        {
            200 => _200ShortCircuitMetadata,
            401 => _401ShortCircuitMetadata,
            404 => _404ShortCircuitMetadata,
            null => _nullShortCircuitMetadata,
            _ => new ShortCircuitMetadata(statusCode)
        };

        // Add the ShortCircuitMetadata instance to the endpoint 
        builder.Add(b => b.Metadata.Add(metadata));
        return builder;
    }
}

A continuación, veremos el método de extensión en RouteShortCircuitEndpointRouteBuilderExtensions.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing;

public static class RouteShortCircuitEndpointRouteBuilderExtensions
{
    // This is the no-op RequestDelegate executed for the endpoints
    private static readonly RequestDelegate _shortCircuitDelegate = (context) => Task.CompletedTask;

    // You can pass multiple route-prefixes to be handled and
    // each one is converted to an endpoint
    public static IEndpointConventionBuilder MapShortCircuit(
        this IEndpointRouteBuilder builder, int statusCode, params string[] routePrefixes)
    {
        // wrap all the endpoints in a group
        var group = builder.MapGroup("");
        foreach (var routePrefix in routePrefixes)
        {
            string route = routePrefix.EndsWith("/", StringComparison.OrdinalIgnoreCase)
                ? $"{routePrefix}{{**catchall}}"
                : $"{routePrefix}/{{**catchall}}";
                               //👆 normalise the route to end with /{**catchall}

            // Map each of the route prefixes with the no-op handler
            group.Map(route, _shortCircuitDelegate)
                .ShortCircuit(statusCode) // Mark the request as short-circuited
                .Add(endpoint =>
                {
                    // update the name of the endpoint
                    endpoint.DisplayName = $"ShortCircuit {endpoint.DisplayName}";
                    // make sure the route is last (as it's a catch-all route)
                    ((RouteEndpointBuilder)endpoint).Order = int.MaxValue;
                });
        }

        // Return the group as an IEndpointConventionBuilder so you
        // can add additional metadata if you wish
        return new EndpointConventionBuilder(group);
    }
}

Por lo tanto, MapShortCircuit(), la función, crea un punto de conexión de ruta general para cada prefijo de ruta, incorpora el parámetro ShortCircuitMetadata y actualiza los metadatos del punto de conexión. Esto nos conduce finalmente a las modificaciones en el RoutingMiddleware, donde tiene lugar el cortocircuito. Después de que el middleware ha seleccionado un punto de conexión, verifica si el punto de conexión tiene ShortCircuitMetadata y, en caso afirmativo, ejecuta el punto de conexión y retorna:

var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
if (shortCircuitMetadata is not null)
{
    return ExecuteShortCircuit(shortCircuitMetadata, endpoint, httpContext);
}

La ejecución del punto de cortocircuito se presenta a continuación: no me he esforzado por organizar esto de ninguna manera, así que, en resumen, podemos destacar:

  • Comprueba que el punto de conexión es válido para ser cortocircuitado
  • Establezca el código de estado en el valor de la variable ShortCircuitMetadata
  • Ejecuta el comando RequestDelgate para el punto de conexión.
  • Registre los detalles si es necesario.
private Task ExecuteShortCircuit(ShortCircuitMetadata shortCircuitMetadata,
    Endpoint endpoint, HttpContext httpContext)
{
    // This check mirrors the implementation in EndpointMiddleware
    if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
    {
        // If you try to short circuit an endpoint with 
        // authorization, CORS, or antiforgery metadata
        // it throws an exception
        if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null)
        {
            ThrowCannotShortCircuitAnAuthRouteException(endpoint);
        }

        if (endpoint.Metadata.GetMetadata<ICorsMetadata>() is not null)
        {
            ThrowCannotShortCircuitACorsRouteException(endpoint);
        }

        if (endpoint.Metadata.GetMetadata<IAntiforgeryMetadata>() is { RequiresValidation: true } &amp;&amp;
            httpContext.Request.Method is {} method &amp;&amp;
            HttpExtensions.IsValidHttpMethodForForm(method))
        {
            ThrowCannotShortCircuitAnAntiforgeryRouteException(endpoint);
        }
    }

    // Set the status code that was recorded when the endpoint was mapped
    if (shortCircuitMetadata.StatusCode.HasValue)
    {
        httpContext.Response.StatusCode = shortCircuitMetadata.StatusCode.Value;
    }

    // Execute the endpoint (optionally with logging)
    if (endpoint.RequestDelegate is not null)
    {
        if (!_logger.IsEnabled(LogLevel.Information))
        {
            // Avoid the AwaitRequestTask state machine allocation if logging is disabled.
            return endpoint.RequestDelegate(httpContext);
        }

        Log.ExecutingEndpoint(_logger, endpoint);

        try
        {
            var requestTask = endpoint.RequestDelegate(httpContext);
            if (!requestTask.IsCompletedSuccessfully)
            {
                return AwaitRequestTask(endpoint, requestTask, _logger);
            }
        }
        catch
        {
            Log.ExecutedEndpoint(_logger, endpoint);
            throw;
        }

        Log.ExecutedEndpoint(_logger, endpoint);

        return Task.CompletedTask;

        static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
        {
            try
            {
                await requestTask;
            }
            finally
            {
                Log.ExecutedEndpoint(logger, endpoint);
            }
        }

    }
    else
    {
        Log.ShortCircuitedEndpoint(_logger, endpoint);
    }
    return Task.CompletedTask;
}

Esta constituye la totalidad de la implementación de la función de cortocircuito. Aunque no presenta innovaciones destacadas, debería aligerar la carga de los puntos finales comunes sin operación, lo cual supone una pequeña ventaja.

Conclusiones

En este artículo, he detallado la reciente adición a .NET 8: la característica de enrutamiento de cortocircuito. Un punto final de cortocircuito se ejecuta directamente en el RoutingMiddleware, en lugar de hacerlo en el EndpointMiddleware. Esta funcionalidad resulta valiosa al eludir la ejecución de middleware de autorización o CORS para puntos de conexión que no requieren estas características o que están destinados a devolver un código 404. Al final del artículo, proporcioné una visión detallada sobre cómo se implementa esta función, destacando el uso de metadatos de punto final. Este enfoque específico demuestra la versatilidad y la eficacia del enrutamiento de cortocircuito, ofreciendo a los desarrolladores una herramienta valiosa para optimizar el rendimiento y la eficiencia en sus aplicaciones ASP.NET Core.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *