Introduction

URL routing is a fundamental part of ASP.NET Core applications, determining how incoming requests are mapped to controller actions. While the framework provides numerous built-in route constraints like int, bool, and guid, there are situations where you need more sophisticated validation rules for your URL parameters. This is where custom route constraints become invaluable.

In this guide, we’ll explore how to create and implement custom route constraints in ASP.NET Core using the IRouteConstraint interface. By the end, you’ll understand how to validate URL parameters with complex business rules specific to your application.

Understanding Route Constraints

Route constraints in ASP.NET Core allow you to restrict how URL parameters are matched when routing incoming requests. They’re specified in route templates using the syntax {parameter:constraint}, where constraint defines the validation rule.

When the ASP.NET Core routing system processes an incoming request, it evaluates these constraints to determine if the URL matches a defined route. If any constraint fails, the route is not matched, and the framework continues searching for a matching route or returns a 404 error if none is found.

The IRouteConstraint Interface

At the heart of custom route constraints is the IRouteConstraint interface. It’s pretty straightforward, you only need to implement a single method called Match:

public interface IRouteConstraint
{
    bool Match(
        HttpContext httpContext,
        IRouter route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection);
}

Don’t let all those parameters intimidate you. They’re actually quite useful:

  • httpContext gives you access to the current request, which can be handy if you need to check headers or other request data
  • route references the route this constraint is attached to
  • routeKey is just the name of your parameter (like “id” in “{id:alpha}”)
  • values contains all the route values from the URL, this is where you’ll find the actual value to validate
  • routeDirection tells you if the framework is matching an incoming URL or generating a URL (most of the time you’ll focus on incoming requests)

Creating a Custom Route Constraint

Let’s implement a simple but useful custom route constraint that checks if a parameter contains only alphabetical characters:

public class AlphaConstraint : IRouteConstraint
{
    public bool Match(
        HttpContext httpContext,
        IRouter route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        // Check if the parameter exists and has a value
        if (!values.TryGetValue(routeKey, out var value) || value == null)
        {
            return false;
        }

        // Verify the value is a string and contains only letters
        var paramValue = value.ToString();
        return !string.IsNullOrEmpty(paramValue) && paramValue.All(char.IsLetter);
    }
}

This constraint ensures that the parameter:

  1. Exists in the route values
  2. Is not null
  3. Contains only alphabetical characters (A-Z, a-z)

Registering Your Custom Constraint

Before you can use your custom constraint in route templates, you need to register it with the routing system. In modern .NET 8/9 applications, this is done in the Program.cs file:

// In Program.cs (.NET 8/9)
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();

// Register the custom constraint
builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("alpha", typeof(AlphaConstraint));
});

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

The ConstraintMap is a dictionary that maps constraint names to their implementing types. After registration, you can use your custom constraint in route templates using the name you provided (in this case, “alpha”).

Using Custom Constraints in Routes

Once registered, you can use your custom constraint in various routing scenarios:

In Controller Route Attributes

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:alpha}")]
    public IActionResult GetProduct(string id)
    {
        // Only executes if id contains only letters
        return Ok($"Product with alphabetic ID: {id}");
    }

    [HttpGet("code/{code:alpha:length(6)}")]
    public IActionResult GetProductByCode(string code)
    {
        // Only executes if code contains exactly 6 letters
        return Ok($"Product with code: {code}");
    }
}

In Conventional Routing

app.MapControllerRoute(
    name: "products",
    pattern: "products/{category:alpha}/{id:int}",
    defaults: new { controller = "Products", action = "GetByCategoryAndId" }
);

In Minimal API Routes

// In Program.cs with .NET 8/9 minimal APIs
app.MapGet("api/products/{sku:alpha:length(10)}", (string sku, HttpContext context) =>
{
    // Access the current user with new .NET 8/9 Identity features
    var user = context.User;

    // Use typed results for better API documentation
    return TypedResults.Ok(new { Sku = sku, LookupTime = DateTime.UtcNow });
});

Creating a More Complex Custom Constraint

Let’s implement a more practical constraint that validates a product code with a specific format (three letters followed by four digits):

public class ProductCodeConstraint : IRouteConstraint
{
    // Regex for 3 letters followed by 4 digits
    private static readonly Regex _regex =
        new Regex(@"^[A-Za-z]{3}[0-9]{4}$", RegexOptions.Compiled);

    public bool Match(
        HttpContext httpContext,
        IRouter route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var value) || value == null)
        {
            return false;
        }

        var valueString = value.ToString();
        return !string.IsNullOrEmpty(valueString) &&
               _regex.IsMatch(valueString);
    }
}

After registering this constraint as “productCode”:

options.ConstraintMap.Add("productCode", typeof(ProductCodeConstraint));

You can use it in your routes:

[HttpGet("products/{code:productCode}")]
public IActionResult GetProductByCode(string code)
{
    // Only executes for product codes that match the format (e.g., ABC1234)
    return Ok($"Found product with code: {code}");
}

Combining Multiple Constraints

One powerful feature of ASP.NET Core routing is the ability to combine multiple constraints for a single parameter. This allows for more precise route matching:

// Matches only if id is alphabetic and between 5-10 characters
[HttpGet("products/{id:alpha:length(5,10)}")]
public IActionResult GetProduct(string id)
{
    return Ok($"Product ID: {id}");
}

You can also combine custom and built-in constraints:

// Using our custom constraint with built-in min/max constraint
[HttpGet("orders/{region:alpha:minlength(2):maxlength(3)}")]
public IActionResult GetOrdersByRegion(string region)
{
    return Ok($"Orders for region: {region}");
}

Using Constraints with Dependency Injection

For more advanced scenarios, your custom constraint might need access to services registered in the dependency injection (DI) container. Here’s how to implement a constraint that uses DI in .NET 8/9:

public class ValidCompanyConstraint : IRouteConstraint
{
    private readonly ICompanyService _companyService;

    public ValidCompanyConstraint(ICompanyService companyService)
    {
        _companyService = companyService;
    }

    public bool Match(
        HttpContext httpContext,
        IRouter route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var value) || value == null)
        {
            return false;
        }

        string companyId = value.ToString();

        // Use the injected service to validate the company ID
        // Leveraging new async-over-sync pattern with Task.WaitAsync for timeout support
        try
        {
            return _companyService.CompanyExistsAsync(companyId)
                .WaitAsync(TimeSpan.FromSeconds(1))
                .GetAwaiter()
                .GetResult();
        }
        catch (TimeoutException)
        {
            // Log timeout and fail gracefully
            return false;
        }
    }
}

Register this constraint in your .NET 8/9 application:

// Register your service with the appropriate lifetime
builder.Services.AddScoped<ICompanyService, CompanyService>();

// Register the constraint with dependency resolution
builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("validCompany", serviceProvider =>
        new ValidCompanyConstraint(
            serviceProvider.GetRequiredService<ICompanyService>()));
});

Best Practices for Custom Route Constraints

When implementing custom route constraints, follow these best practices:

  1. Keep constraints focused: Each constraint should validate a single aspect of the parameter.

  2. Consider performance: Since constraints are evaluated for every request, optimize for performance. For complex regex patterns, use the RegexOptions.Compiled flag.

  3. Handle null values properly: Always check if the parameter exists and has a value before validation.

  4. Use meaningful names: Choose constraint names that clearly describe their purpose (e.g., “zipCode”, “ssn”).

  5. Document constraints: Add XML comments to explain what each constraint does and what format it expects.

  6. Consider case sensitivity: Be explicit about whether your constraint is case-sensitive.

  7. Unit test your constraints: Ensure your constraints work correctly in isolation and with different inputs.

Conclusion

Custom route constraints in ASP.NET Core provide a powerful way to validate URL parameters according to specific business rules. By implementing the IRouteConstraint interface, you can create constraints that perfectly match your application’s requirements.

Custom constraints improve your application in several ways:

  • They enforce a consistent URL structure across your API
  • They prevent unnecessary processing of invalid requests
  • They make your routing self-documenting by clearly defining acceptable parameter formats
  • They reduce boilerplate validation code in your controllers

With the knowledge gained from this guide, you can now create sophisticated routing rules that enhance both the security and usability of your ASP.NET Core applications. Remember that well-designed URLs are an essential part of a professional web API, and custom constraints help ensure your routes follow your application’s business rules.