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 rutaid
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 usarvalues[routeKey]
para obtener el valor del parámetro que nos interesa comprobar.routeDirection
indica si esta ejecución del métodoMatch()
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:
Hola, es posible implementar con 'nullables' como en Net Framework?
Digamos:
ShowDetails2(int? id){...}
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!
Enviar un nuevo comentario
Backlinks: