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, 27 de junio de 2023
ASP.NET Core

Cuando en ASP.NET Core MVC usamos rutado por convención, lo habitual es que accedamos a las acciones mediante rutas definidas en el patrón, como [controller]/[action]. Así, podemos encontrarnos con rutas como /PendingInvoices/ViewAll para acceder a la siguiente acción:

public class PendingInvoicesController : Controller
{
    public IActionResult ViewAll() => Content("Show all pending invoices");
}

Lo mismo ocurre con páginas Razor. Si usamos las rutas por defecto, al archivo /Pages/ShowAllPendingInvoices.cshtml podríamos acceder mediante la ruta /ShowAllPendingInvoices. No es que sean rutas terribles, pero tampoco podemos decir que sean lo mejor del mundo en términos de legibilidad y conveniencia.

El kebab-casing consiste en separar con un guion "-" las distintas palabras que componen los fragmentos de la ruta, por lo que en los casos anteriores tendríamos /pending-invoices/view-all y show-all-pending-invoices, algo bastante más legible, elegante, y apropiado desde el punto de vista del SEO.

El nombre kebab-casing viene de que visualmente el resultado es similar a un pincho atravesando trozos de comida. Imaginación que no falte 😉

En este post vamos a ver cómo aprovechar los puntos de extensibilidad del sistema de routing de ASP.NET Core para modificar la forma en que genera rutas y así adaptarlo a nuestras necesidades.

Kebab-case en rutado por convención de acciones MVC

La interfaz IOutboundParameterTransformer, proporcionada por ASP.NET Core, especifica las operaciones que debe implementar una clase para poder transformar los parámetros cuando está siendo generada una URL.

Su definición es la siguiente:

public interface IOutboundParameterTransformer : IParameterPolicy
{
    string? TransformOutbound(object? value);
}

El método TransformOutbound() recibe el valor de un parámetro de ruta, y retorna el valor transformado. Este método es invocado sucesivas veces durante la inicialización de la aplicación ASP.NET Core para darnos la oportunidad de transformar los parámetros utilizados para la definición de las rutas de acceso a las acciones MVC detectadas.

Y como podréis intuir, este es sin duda un buen punto para introducir la lógica de conversión a kebab-case. Podemos conseguirlo con una clase como la siguiente:

public class KebabCaseParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value == null) 
            return null;
        var str = value.ToString();
        if (string.IsNullOrEmpty(str)) 
            return null;
        return Regex.Replace(str, "([a-z])([A-Z])", "$1-$2").ToLower();
    }
}

Ahora faltaría conseguir que esta clase sea tenida en cuenta durante la inicialización de la aplicación de ASP.NET Core. Para ello, en primer lugar, podemos registrar una restricción personalizada en el momento de añadir los servicios de routing al inyector de dependencias, en el Program.cs:

...
builder.Services.AddRouting(options =>
    options.ConstraintMap["kebab"] = typeof(KebabCaseParameterTransformer));
...    

Y luego, en el mismo Program.cs, al definir el patrón de ruta por defecto será necesario incluir esta restricción:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:kebab=Home}/{action:kebab=Index}/{id?}");

Hecho esto, las acciones accesibles a través de la ruta por defecto serán añadidas a la colección de endpoints mediante las rutas transformadas, es decir, que habrán pasado previamente por nuestro método TransformOutbound().

De la misma forma, los distintos métodos de generación de rutas hacia acciones, como Html.Action(), LinkGenerator o los tag helpers de generación de enlaces, ya tendrán en cuenta también esta transformación. Por ejemplo, el siguiente tag helper en una vista generará un enlace correcto hacia /pending-invoices/view-all:

<a asp-controller="PendingInvoices" asp-action="ViewAll">View all</a>

Bueno, pues parece que todo marcha bien, aunque todavía nos quedan varios asuntos por resolver: el rutado por atributos y las páginas Razor. Vamos con ello.

Kebab-case en rutado por atributos de acciones MVC

Es lógico que la solución anterior no funcione con acciones o controladores que usan rutado por atributos, porque las restricciones las estamos teniendo en cuenta exclusivamente en el mapeo de la ruta por defecto y, como sabemos, el rutado mediante atributos va por otras vías.

Si queremos que funcione también en este caso, debemos añadir una convención para que el transformador KebabCaseParameterTransformer sea aplicado a los tokens de las rutas. Esto lo podemos hacer en el momento de añadir los servicios de controladores en el código de inicialización:

builder.Services.AddControllersWithViews()
    .AddMvcOptions(opt =>
    {
        opt.Conventions.Add(
            new RouteTokenTransformerConvention(new KebabCaseParameterTransformer()));
    });

Una vez hecho esto, ya podremos definir las rutas mediante atributos, y los tokens controller y action se verán afectados por el transformador, generando siempre su versión kebab-case:

[Route("[controller]/[action]")]
public class PendingInvoicesController : Controller
{
    public IActionResult ViewAll() => Content("Show all pending invoices");
}

Kebab-case en Razor Pages

Siempre que no se indique un path específico mediante la directiva @page, las Razor Pages utilizan como ruta la ubicación y nombre del archivo .cshtml en el sistema de ficheros.

Por tanto, si nuestra Razor Page se encuentra físicamente en el archivo /ComputersAndDevices/DesktopAndLaptop/AdvancedSearch.cshtml, la ruta para acceder a ella será normalmente /ComputersAndDevices/DesktopAndLaptop/AdvancedSearch.

Para convertir en kebab esa ruta, de nuevo tendremos que añadir una convención en la inicialización de la aplicación, justo después de añadir los servicios de Razor Pages:

builder.Services.AddRazorPages()
  .AddRazorPagesOptions(options => {
      options.Conventions.Add(
        new PageRouteTransformerConvention(new KebabCaseParameterTransformer()));
    }
  );

Al igual que ocurría con las acciones MVC, el método TransformOutbound() será invocado durante la inicialización de la aplicación ASP.NET Core pasándole cada fragmento de la ruta de acceso hasta el archivo, dándonos la oportunidad de modificarlo. Dado que en nuestra lógica reemplazamos el valor original por su kebab correspondiente, estaremos modificando su ruta de acceso.

Por tanto, una vez hecho esto, podremos acceder a la página usando la ruta más amigable como /computers-and-devices/desktop-and-laptop/advanced-search.

También esta convención se tendrá en cuenta a la hora de generar las rutas o enlaces. Por ejemplo, si en una vista introducimos un código como el siguiente, la ruta generada realmente en el enlace será la correcta, ya kebabizada:

<a asp-page="/ComputersAndDevices/DesktopAndLaptop/AdvancedSearch">Search</a>

Publicado en Variable not found.

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