Vaughan

Using route constraints in ASP.NET Core

.NET Core

A small but useful feature available in ASP.NET Core is to define route constraints to disambiguate similar routes. Its important to note that the documentation makes it clear that you shouldn't be using this for input validation.

The main use case is if you want different actions using the same route but business logic requires that specific actions should be used based on the values provided. To be honest I haven't seen this used much but its always good to have another tool in your toolkit.

The example below has 3 actions with the same route. They each have a constraint to handle the call depending on what value is passed into the route.

[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
    [Route("byage/{age:range(18,64)}")]
    public async Task<IActionResult> Adult(int age)
    {
        //handle adult
        return Ok(age);
    }

    [Route("byage/{age:min(65)}")]
    public async Task<IActionResult> Retired(int age)
    {
        //handle retired
        return Ok(age);
    }

    [Route("byage/{age:max(17)}")]
    public async Task<IActionResult> Children(int age)
    {
        //handle children
        return Ok(age);
    }
}

If you call /api/Person/byage/2 you will reach the Children action but if you call /api/Person/byage/50 you will reach the Adult action.

One thing you need to be careful about is if two route constraints match on specific calls. For example the Adult constraint was changed to range(17,64) then /api/Person/byage/17 will fail because it will match two actions.

There is quite a few built in route constraints but its also easy to create your own if you need something different.

I'm going to create a Name constraint that will match if the name parameter is the predefined route constraint. The example controller will look like this.

[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
    [Route("byname/{name:Name(steve)}")]
    public async Task<IActionResult> Steve(string name)
    {
        //Handle Steve
        return Ok(name);
    }

    [Route("byname/{name:Name(john)}")]
    public async Task<IActionResult> John(string name)
    {
        //Handle John
        return Ok(name);
    }
}

I want the call to api/Person/byname/john to hit the John action and the call to api/Person/byname/steve to hit the Steve action.

This is how you can create the constraint in your solution. Its expecting a name parameter in the route, which will match against the name passed into its constructor.

public class NameConstraint : IRouteConstraint
{
    readonly string _name;

    public NameConstraint(string name)
    {
        _name = name;
    }

    public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        string name = values["name"].ToString();

        if(_name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
        {
            return true;
        }
        return false;
    }
}

You then just need to add it to the RouteOptions in Startup ConfigureServices and you are ready to use it.


public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.Configure<RouteOptions>(options =>
    {
        options.ConstraintMap.Add("Name", typeof(NameConstraint));
    });
}

Related Posts

BMC logoBuy me a coffee