martes, 18 de febrero de 2020
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.
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.
Estas clases transformadoras deben heredar de
Publicado en Variable not found.
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étodoConfigure()
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
ocontroller
. - 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.
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 deDynamicRouteValueTransformer
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!
Enviar un nuevo comentario