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, 18 de febrero de 2020
ASP.NET Core MVC En muchas ocasiones, la llegada de una nueva versión de un framework viene acompañada de documentación, posts y otro tipo de contenidos que anuncian a bombo y platillo las principales novedades y cambios introducidos. Ante este ruido, es fácil pasar por alto otros pequeños cambios que introducen mejoras a los marcos de trabajo o simplemente permiten ir evolucionando código antiguo a funcionalidades más recientes.

Hoy vamos a hablar de una de eso pequeños descubrimientos: el rutado dinámico de controladores. Introducido en ASP.NET Core 3, se trata de una característica que no ha sido especialmente publicitada ni comentada, pero puede ser de utilidad en algunas ocasiones, pues permite interferir en la forma de interpretar las rutas de las peticiones entrantes con objeto de decidir en cada caso cómo deben ser procesadas.

Rutado estático vs rutado dinámico

Seguro que muchos estáis ya empezando a utilizar el flamante endpoint routing para definir las rutas hacia controladores o páginas Razor de vuestras aplicaciones y reconoceréis el siguiente código, perteneciente al método Configure() de la clase Startup, que configura las rutas por defecto para los controladores MVC de una aplicación:
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
En esta definición estamos estableciendo una férrea relación entre un patrón de ruta y la ubicación de los endpoints que procesarán las peticiones que encajen con el mismo. Estas definiciones, grabadas a fuego durante el arranque de la aplicación, dan poco margen para la sorpresa: por ejemplo, una petición hacia "/products/show" será procesada desde la acción "Show" del controlador "Products" inexorablemente.

Sin embargo, hay escenarios en los que necesitamos más flexibilidad y queremos ser nosotros los que seleccionemos el controlador y acción que procesará una petición, manipular los parámetros de ruta existentes o incluso añadir parámetros de ruta personalizados. Estas posibilidades son las que se abren con el rutado dinámico de ASP.NET Core 3.
Algunos probablemente habréis hecho este tipo de cosas mediante implementaciones personalizadas de IRouter: tened en cuenta este mecanismo ya no es compatible con endpoint routing y habría que migrarlo a este nuevo enfoque.

Transformaciones de valores de ruta

En la práctica, el rutado dinámico se implementa en clases transformadoras de valores de ruta, componentes personalizados que serán invocados en cada petición para darles la oportunidad de modificar a su antojo los parámetros de ruta.

Estas clases transformadoras deben heredar de DynamicRouteValueTransformer, una abstracción proporcionada por el framework que tiene la siguiente pinta:
public abstract class DynamicRouteValueTransformer
{  
    public abstract ValueTask<RouteValueDictionary> TransformAsync(
        HttpContext httpContext,
        RouteValueDictionary values);
}
Cuando heredemos de ella para crear nuestra transformación personalizada, el método TransformAsync() recibirá el objeto HttpContext y los valores de parámetros de ruta que han sido detectados hasta este momento en forma de diccionario. El objetivo de las transformaciones que implementaremos en la clase es alterar el contenido del diccionario de parámetros de ruta para que el comportamiento se adapte a nuestras necesidades, por ejemplo:
  • Si quisiéramos modificar la acción o controlador que procesaría una petición, sólo tendríamos que alterar el valor de los parámetros de ruta action o controller.
  • Podríamos añadir nuevos parámetros de ruta en función de los datos de entrada.
  • Podríamos reemplazar valores por otros, por ejemplo para traducirlo a una cultura neutra, des-sluggizarlo o transformarlo de alguna forma.
Una vez realizadas las modificaciones que consideremos sobre el diccionario de rutas, simplemente retornaremos una referencia hacia el mismo. Por tanto, el esquema de una implementación personalizada de DynamicRouteValueTransformer podría ser como el siguiente:
public class MyCustomTransformer : DynamicRouteValueTransformer
{
    public override async ValueTask<RouteValueDictionary> TransformAsync(
        HttpContext httpContext, RouteValueDictionary values)
    {
        // Modificamos la colección de parámetros de ruta, por ejemplo:
        // values["controller"]="Products"
        return values;
    }
}
Para que una clase transformadora pueda ser utilizada, debemos registrarla previamente en el método ConfigureServices(). Podemos utilizar cualquier tipo de lifetime, por lo que somos libres de elegir el que más convenga:
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddScoped<MyCustomTransformer>();
}
Fijaos que lo interesante de esto es que, dado que sus instancias son suministradas por el proveedor de servicios, podríamos utilizar inyección de dependencias en su constructor, :
public class MyCustomTransformer : DynamicRouteValueTransformer
{
    private readonly IMyService _service;
    public LocalizationTransformer(IMyService service)
    {
        _service = service;
    }
    public override ValueTask<RouteValueDictionary> TransformAsync(
        HttpContext httpContext, RouteValueDictionary values)
    {
        // Usamos "service" para alterar los parámetros de ruta
    }
}
Además, a la hora de registrar los endpoints tendremos que usar el extensor MapDynamicControllerRoute, indicar la clase DynamicRouteValueTransformer donde hemos implementado la lógica de transformación, y especificar el patrón de ruta:
app.UseEndpoints(endpoints =>
{
    endpoints.MapDynamicControllerRoute<MyRouteTransformer>(
        "{controller=Home}/{action=Index}/{id?}");
});
Nota: de la misma forma, podemos utilizar MapDynamicPageRoute() si lo que queremos es intervenir en las peticiones dirigidas a páginas Razor.

Ejemplo 1: localización simple de rutas

Veamos un ejemplo de DynamicRouteValueTransformer que implementa una sencilla transformación para traducir el controlador y acción a un idioma neutro, de forma que peticiones como "/products/view" o "/productos/ver" puedan ser procesadas desde ProductsController.View():
public class LocalizationTransformer : DynamicRouteValueTransformer
{
    public override ValueTask<RouteValueDictionary> TransformAsync(
        HttpContext httpContext, RouteValueDictionary values)
    {
        var controllerName = (string)values["controller"];
        if ("productos".Equals(controllerName, StringComparison.InvariantCultureIgnoreCase))
        {
            values["controller"] = "products";
        }
        var actionName = (string)values["action"];
        if ("ver".Equals(actionName, StringComparison.InvariantCultureIgnoreCase))
        {
            values["action"] = "view";
        }
        return new ValueTask<RouteValueDictionary>(values);
    }
}
Aunque el ejemplo anterior es bastante simple porque está todo hardcodeado, obviamente podríamos crear soluciones bastante más potentes si tenemos en cuenta que la clase podría recibir dependencias y que en el método TransformAsync() tenemos toda la información del contexto disponible:
public class LocalizationTransformer : DynamicRouteValueTransformer
{
    private readonly ILocalizationServices _loc;
    public LocalizationTransformer(ILocalizationServices loc)
    {
        _loc = loc;
    }
    public override ValueTask<RouteValueDictionary> TransformAsync(
        HttpContext httpContext, RouteValueDictionary values)
    {
        // Usar _loc para acceder a las traducciones:
        // values["controller"] = await _loc.TranslateAsync(values["controller"]);
        return values;
    }
}

Ejemplo 2: selección dinámica del controlador

Supongamos que queremos que nuestra aplicación reciba un identificador de producto a través de una ruta con el patrón "/product/{id}/{action}" y que nos interesa que el controlador que procese estas peticiones dependa del tipo de producto (ya, sería un interés algo extraño, pero bueno...). Podríamos crear algo así:
public class ProductTypeTransformer : DynamicRouteValueTransformer
{
    private readonly IProductRepository _productRepository;

    public ProductTypeTransformer(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public override async ValueTask<RouteValueDictionary> TransformAsync(
        HttpContext httpContext, RouteValueDictionary values)
    {
        if (int.TryParse(values["id"].ToString(), out var id))
        {
            var product = await _productRepository.GetByIdAsync(id);
            if (product != null)
            {
                var controller = product.Type switch
                {
                    ProductTypes.Laptops => "laptops",
                    ProductTypes.DesktopComputers => "desktop",
                    ProductTypes.Tablets => "tablet",
                    ProductTypes.Phones => "phone",
                    ProductTypes.Tv => "tv",
                    _ => "generic",
                };
                values["controller"] = controller;
            }
        }
        return values;
    }
}
Sencillo, ¿verdad? Como decía al principio, no es una feature para usarla todos los días, pero está bien saber que existe por si encontramos un escenario en el que pudiera venir bien esta flexibilidad para adaptar el routing a nuestras necesidades.

Publicado en Variable not found.

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