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, 4 de octubre de 2022
ASP.NET Core

Las inline route constraints, o restricciones de ruta en línea son un interesante mecanismo de ASP.NET Core para especificar condiciones sobre los parámetros definidos en el interior de los patrones de ruta.

Por ejemplo, una acción MVC o un endpoint mapeado usando el patrón /product/{id}, será ejecutado cuando entren peticiones hacia las rutas /product/1 y product/xps-15. Sin embargo, si en el momento del mapeo utilizamos el patrón /product/{id:int} estaremos indicando que el manejador sólo debe ser ejecutado cuando el valor del parámetro id sea un entero.

Esto podemos verlo mejor en un ejemplo. Observad la definición de la siguiente acción MVC, que será ejecutada sólo si el valor para el parámetro id es un entero, es decir, responderá a peticiones como /product/1 o /product/234, pero será ignorada si la petición entrante se dirige a la ruta /product/xps-15:

public class ProductController : Controller
{
    ...
    [HttpGet("product/{id:int}")]
    public async Task<ActionResult<Product>> ShowDetails(int id)
    {
        var product = ... // Obtener producto
        return product != null ? product:  NotFound();
    }
}

O lo que sería su equivalente usando las minimal APIs introducidas en .NET 6:

app.MapGet("/product/{id:int}", async (int id) =>
{
    var product = await new ProductCatalog().GetByIdAsync(1); // Obtener producto
    return product != null ? Results.Ok(product) : Results.NotFound();
});

Como ya habréis adivinado, en {id:int} es donde estamos especificando que el parámetro de ruta id debe ser entero.

ASP.NET Core viene equipado de fábrica con un buen número de restricciones que podemos usar a la hora de definir las rutas de nuestras aplicaciones. Las más interesantes son:

constraint Ejemplo de uso Ejemplo de valor correcto Notas
int {id:int} 123 Valor entero
bool {active:bool} true Debe ser "true" o "false"
datetime {dateofbirth:datetime} 2016-01-01 Valor de tipo fecha (cultura invariable)
decimal {price:decimal} 49.99 Valor decimal válido (cultura invariable)
double {weight:double} 4.234 Valor double válido (cultura invariable)
float {weight:float} 3.14 Valor float válido
guid {id:guid} 7342570B... GUID válido
long {ticks:long} 123456789 Entero largo válido
minlength(value) {username:minlength(5)} steve Cadena con 5 caracteres mínimo
maxlength(value) {filename:maxlength(8)} somefile Cadena con 8 caracteres máximo
length(value) {pin:length(4)} 1abc Cadena de exactamente 4 caracteres
length(min,max) {filename:length(4,16)} Somefile.txt Cadena entre 4 y 6 caracteres
min(value) {age:min(18)} 19 El valor debe ser como mínimo 18
max(value) {age:max(120)} 91 El valor debe ser como máximo 120
range(min,max) {age:range(18,120)} 91 El valor debe estar entre 18 y 120
alpha {name:alpha} Steve La cadena debe contener exclusivamente caracteres alfabéticos
regex(expression) {ssn:regex(\^d{3}-d{2}-d{4}$)} 123-45-6789 La cadena debe cumplir la expresión regular

Pero, como casi siempre en el framework, podemos crear nuestras propias restricciones para adaptar este mecanismos a nuestras necesidades.

Creación de una restricción personalizada

El proceso de creación de restricciones personalizadas es muy sencillo, y consiste únicamente en dos pasos:

  • crear la restricción en una clase que implemente IRouteConstraint,
  • y registrarla para que esté disponible en el sistema de routing de ASP.NET Core.

Crearemos una restricción sencilla, a la que llamaremos palindrome, que comprobará que los valores de entrada que sean palíndromos. Así pues, una definición de ruta como /test/{str:palindrome} encajará con peticiones como /test/arañara o /test/reconocer, pero no con /test/hola.

Veamos primero la clase, a la que llamamos PalindromeConstraint:

public class PalindromeConstraint : IRouteConstraint
{
    public bool Match(HttpContext httpContext, IRouter route, string routeKey, 
                      RouteValueDictionary values, RouteDirection routeDirection)
    {
        // Obtenemos el valor del parámetro de ruta
        string value = values[routeKey]?.ToString();

        // Vale, no es la implementación más óptima del mundo para
        // detectar palíndromos, pero nos vale para nuestro ejemplo ;)
        return value?.SequenceEqual(value.Reverse()) ?? false;
    }
}

Como hemos comentado algo más arriba, la clase debe implementar la interfaz IRouteConstraint, lo que nos obligará a definir el método Match(), en el que se definen los los siguientes parámetros:

  • httpContext, con el contexto de la petición.
  • route es el objeto en el que está definida la ruta (no se suele usar).
  • routeKey es el nombre del parámetro al que se aplica la restricción. Por ejemplo, en una plantilla de ruta como /test/{str:palindrome}, llegaría el valor "str".
  • values contiene un diccionario con los valores de los parámetros de ruta, poblados a partir de la URL de la petición entrante. Como podemos ver, podemos usar values[routeKey] para obtener el valor del parámetro que nos interesa comprobar.
  • routeDirection indica si esta ejecución del método Match() se debe a una petición entrante o a la generación de una URL.

Una vez definida la clase, debemos registrarla en el código de inicialización de la aplicación, en Program.cs:

var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddRouting(options =>
{
    options.ConstraintMap.Add("palindrome", typeof(PalindromeConstraint));
});

Básicamente, lo que hemos hecho es crear una asociación entre el identificador "palindrome", que es el que usaremos para especificar la restricción en el interior de una ruta, y la clase PalindromeConstraint, que es donde la implementamos.

A partir de este momento, ya podríamos utilizar la restricción en cualquier punto de la aplicación, ya sea en una minimal API como en MVC:

// Uso en minimal API:
app.MapGet("/test/{str:palindrome}", (string str) => $"{str} is palindrome");

// Uso en MVC:
public class TestController : Controller
{
    [HttpGet("/test/{str:palindrome}")]
    public string Text(string str) => $"{str} is palindrome";
}
Publicado en: www.variablenotfound.com.

2 Comentarios:

Unknown dijo...

Hola, es posible implementar con 'nullables' como en Net Framework?
Digamos:
ShowDetails2(int? id){...}

José María Aguilar dijo...

Hola,

por supuesto, puedes usar tipos anulables como parámetros, o incluso valores por defecto en los parámetros, pero si quieres que a nivel de ruta también sean opcionales tendrás que usar también la interrogación:

/showdetails/{id?}

Saludos!