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, 29 de noviembre de 2022
ASP.NET Core

Hace algunas semanas vimos cómo crear inline route constraints, o restricciones de ruta en línea en ASP.NET Core, y creamos un ejemplo simple que permitía al sistema de routing detectar si el valor suministrado a través de un parámetro de ruta era una palabra palíndroma.

Para ello, creamos la restricción "palindrome" que, implementada en la clase PalindromeConstraint podíamos usar de la siguiente forma:

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

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

Sin embargo, si atendemos a la lista de restricciones disponibles de serie en ASP.NET Core, vemos que hay algunas de ellas que son parametrizadas, como maxlength o range:

Plantilla de ruta Significado
/order/{orderId:minlength(5) orderId debe tener como mínimo 5 caracteres
/setAge/{age:int:range(0,120) age debe ser un entero entre 0 y 120

En este post vamos a ver precisamente eso, cómo crear una restricción personalizada con parámetros.

Creación de restricciones parametrizadas

Como ejemplo, vamos a desarrollar una restricción que permitirá verificar que el valor suministrado sea divisible por el número que indiquemos. Es decir, la idea es poder especificar una restricción como la que usamos en el siguiente ejemplo:

app.MapGet("/test/{value:int:divisibleBy(3)}", (int value) => $"{value} is divisible by 3"); 

Como hicimos en el post anterior, vamos a crear la clase que implementa la restricción. Fijaos que en esta ocasión insertaremos en ella un constructor en el que recibiremos los parámetros de la restricción:

public class DivisibleByConstraint : IRouteConstraint
{
    private readonly int _divisor;

    public DivisibleByConstraint(int divisor)
    {
        _divisor = divisor;
    }

    public bool Match(HttpContext httpContext, IRouter route, string routeKey, 
                      RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!int.TryParse(values[routeKey]?.ToString(), out var intValue))
            return false;
        return intValue % _divisor == 0;
    }
}

Hecho esto, ya podríamos registrarla en el sistema de routing, como siempre, en Program.cs:

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

El tipo del parámetro del constructor debe coincidir con el usado en el momento de insertar la restricción en la ruta. Por ejemplo, en el caso anterior, el constructor espera recibir un entero, por lo que el siguiente mapeo de ruta generará un error en tiempo de ejecución la primera vez que se intente matchear con una petición entrante:

// Esto fallará en tiempo de ejecución, porque el 
// constructor de DivisibleByContraint espera un entero:
app.MapGet("/div/{value:int:divisibleBy(a)}", (int value) => $"{value} is ok");
GET https://localhost:7196/test/6
...
HTTP 500
Microsoft.AspNetCore.Routing.RouteCreationException: An error occurred while trying to 
create an instance of 'DivisibleByConstraint'.
 ---> System.FormatException: Input string was not in a correct format.
... 

También podemos crear constraints que soporten más de un parámetro añadiendo todos los que necesitemos al constructor. Por ejemplo, hemos visto que la restricción range() proporcionada de serie por el framework, permite indicar el rango de valores permitidos; en la práctica, la clase que implementa esta constraint simplemente ha definido un constructor con los dos parámetros entrantes:

public class RangeRouteConstraint: IRouteConstraint
{
    public RangeRouteConstraint(long min, long max)
    {
        ...
    }
}

También existe la posibilidad de usar varios constructores, lo cual puede ser útil si queremos implementar restricciones con un número variable de parámetros. Aunque resulta algo farragoso, podemos crear distintos constructores para cubrir los casos que queramos soportar; por ejemplo, a continuación vemos la restricción OneOfContraint que permite verificar si el valor del parámetro de ruta coincide con alguno de los valores (hasta cuatro) que indiquemos en línea:

public class OneOfConstraint : IRouteConstraint
{
    private readonly HashSet<string> _allowedValues = new();

    public OneOfConstraint(string v1) => Add(v1);
    public OneOfConstraint(string v1, string v2) => Add(v1, v2);
    public OneOfConstraint(string v1, string v2, string v3) => Add(v1, v2, v3);
    public OneOfConstraint(string v1, string v2, string v3, string v4) => Add(v1, v2, v3, v4);

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        var value = values[routeKey]?.ToString();
        return value != null && _allowedValues.Contains(value);
    }

    private void Add(params string[] args)
    {
        ArgumentNullException.ThrowIfNull(args);
        foreach (var arg in args)
        {
            _allowedValues.Add(arg);
        }
    }
}

Fijaos que debemos utilizar sobrecargas para cada número de parámetros, pues no se permite el uso de params o arrays en el constructor.

Una vez registrada en el sistema de routing, podríamos usar la restricción de la siguiente manera:

app.MapGet("/one-or-two/{value:oneOf(1,2)}", (string value) => $"{value} is 1-2");
app.MapGet("/one-two-or-three/{value:oneOf(1,2,3)}", (string value) => $"{value} is 1-3");

¡Espero que os sea de utilidad!

Publicado en Variable not found.

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