TL;DR

Learn how to build custom route constraints in ASP.NET Core using IRouteConstraint.
Use them to validate route parameters (like ensuring only alphabets or specific patterns), inject services like IUserService, and simplify controller logic.
Bonus: works with minimal APIs, conventional routes, and supports DI in constraints via the new ASP.NET Core routing system.

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 the parameters intimidate you, each one serves a purpose and gives you full control over the request context.

  • 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 from the HttpContext (useful for auth-based constraints)
    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.

[Fact]
public void AlphaConstraint_ShouldReject_NumericValue()
{
    var constraint = new AlphaConstraint();
    var values = new RouteValueDictionary { { "id", "123" } };

    var result = constraint.Match(null, null, "id", values, RouteDirection.IncomingRequest);

    Assert.False(result);
}

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.

Further Reading

References

About the Author

Abhinaw Kumar is a software engineer who builds real-world systems: from resilient ASP.NET Core backends to clean, maintainable Angular frontends. With over 11+ years in production development, he shares what actually works when you're shipping software that has to last.

Read more on the About page or connect on LinkedIn.

Frequently Asked Questions

What is IRouteConstraint in ASP.NET Core?

IRouteConstraint is an interface that lets you create custom rules for validating URL segments in ASP.NET Core routing. It has a single Match method that determines whether a URL parameter meets your specific criteria, returning true if valid and false if not.

When should I use custom route constraints?

Use custom constraints when built-in constraints (like int, bool, regex) aren’t enough for your validation needs. They’re perfect for business-specific formats like product codes, custom IDs, or when you need to validate against dynamic data like database records.

How do I implement a custom route constraint?

Create a class that implements IRouteConstraint, add a Match method that contains your validation logic, register it in Startup using services.AddRouting(options => options.ConstraintMap.Add(“constraintName”, typeof(YourConstraint))), then use it in routes as {parameter-constraintName}.

Can I use dependency injection in custom route constraints?

Yes, ASP.NET Core resolves route constraints from the dependency injection container. Your constraint class can accept dependencies through constructor parameters, which is useful for accessing services like database contexts to validate parameters against existing data.

What’s the difference between route constraints and model validation?

Route constraints validate the URL format before routing to a controller action and prevent invalid routes from matching. Model validation happens after routing and checks the entire input model. Use route constraints for early filtering of requests and model validation for detailed input checking.

When does route constraint validation happen in the request pipeline?

Route constraint validation occurs early in the pipeline during URL matching, before the request reaches your controller. If a constraint fails, the route isn’t matched and ASP.NET Core continues looking for other matching routes or returns 404 if none are found.

Can I create constraints that check against database values?

Yes, but consider performance implications since constraints run on every request. For database-dependent constraints, consider caching frequently used validation data rather than querying the database each time, or use a hybrid approach where the constraint performs basic validation with final checks in the controller.

How do custom constraints work with attribute routing?

Custom constraints work exactly the same with attribute routing as with conventional routing. After registering your constraint, you can use it in route templates within attributes like [Route(“api/[controller]/{id:yourConstraint}”)]. The syntax and behavior are consistent across both routing styles.

Related Posts