Tutorials ASP.NET Core MVC Mastery
Custom Constraints
On this page
Custom Route Constraints in ASP.NET Core — Build Intelligent URL Routing
Built-in route constraints like :int, :guid, and :alpha are powerful — but what happens when your application needs to validate URLs against custom business rules? That's where IRouteConstraint transforms your routing from basic pattern matching into an intelligent request gateway.
1. WHAT Are Route Constraints?
Route constraints are gatekeepers in ASP.NET Core's routing pipeline. They evaluate incoming URL parameters before the request reaches your controller. If a constraint fails, the route is rejected and the framework tries the next matching route — or returns a 404.
ASP.NET Core ships with 15+ built-in constraints:
| Constraint | Example | Purpose |
|---|---|---|
:int | {id:int} | Matches only integer values |
:guid | {token:guid} | Matches GUID format |
:alpha | {name:alpha} | Matches alphabetic characters only |
:minlength(n) | {slug:minlength(3)} | Minimum string length |
:range(m,n) | {age:range(18,120)} | Integer within range |
:regex(pattern) | {code:regex(^[A-Z]{{2,5}}$)} | Matches regex pattern |
:required | {name:required} | Parameter must have a value |
2. WHY Build Custom Constraints?
Built-in constraints are syntactic — they check data types and formats. But real applications require semantic validation:
- Slug Validation: Only allow URL-safe slugs matching
^[a-z0-9-]+$ - Locale Enforcement: Restrict
{lang}to supported locales likeen-us,fr-fr - Enum Matching: Ensure
{status}is one ofactive,inactive,archived - Version Validation: Support API versioning like
v1,v2,v3in the URL
3. HOW to Build a Custom Route Constraint
Every custom route constraint implements the IRouteConstraint interface with a single method: Match().
Step 1: Create the Constraint Class
// Constraints/SlugConstraint.cs
// Validates URLs like /blog/my-first-post (lowercase, numbers, hyphens only)
using System.Text.RegularExpressions;
public class SlugConstraint : IRouteConstraint
{
// Compiled regex for maximum performance in the hot routing path
private static readonly Regex SlugPattern = new(
@"^[a-z0-9]+(?:-[a-z0-9]+)*$",
RegexOptions.Compiled | RegexOptions.CultureInvariant
);
public bool Match(
HttpContext? httpContext,
IRouter? route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out var value) && value is string slug)
{
// Must be 3-100 chars and match slug pattern
return slug.Length >= 3 && slug.Length <= 100 && SlugPattern.IsMatch(slug);
}
return false;
}
}
Step 2: Register in Program.cs
// Program.cs — Register custom constraints BEFORE routing
builder.Services.AddRouting(options =>
{
options.ConstraintMap.Add("slug", typeof(SlugConstraint));
options.ConstraintMap.Add("locale", typeof(LocaleConstraint));
options.ConstraintMap.Add("apiVersion", typeof(ApiVersionConstraint));
});
Step 3: Use in Routes
// Attribute Routing — Clean and readable
[Route("blog/{slug:slug}")]
public IActionResult GetPost(string slug)
{
// slug is guaranteed to match ^[a-z0-9]+(?:-[a-z0-9]+)*$
var post = _blogService.GetBySlug(slug);
if (post == null) return NotFound();
return View(post);
}
// Conventional Routing
app.MapControllerRoute(
name: "blog",
pattern: "blog/{slug:slug}",
defaults: new { controller = "Blog", action = "GetPost" }
);
4. REAL-TIME PRODUCTION EXAMPLES
Example 1: Locale/Language Constraint (Multi-Language App)
// Constraints/LocaleConstraint.cs
public class LocaleConstraint : IRouteConstraint
{
// Supported locales — load from config in production
private static readonly HashSet<string> SupportedLocales = new(StringComparer.OrdinalIgnoreCase)
{
"en-us", "en-gb", "fr-fr", "de-de", "ja-jp", "hi-in", "es-es"
};
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out var value) && value is string locale)
{
return SupportedLocales.Contains(locale);
}
return false;
}
}
// Usage: /en-us/products, /fr-fr/products — invalid locales get 404
[Route("{lang:locale}/products")]
public IActionResult Products(string lang) => View();
Example 2: API Version Constraint
// Constraints/ApiVersionConstraint.cs
public class ApiVersionConstraint : IRouteConstraint
{
// Matches v1, v2, v3 — up to v99
private static readonly Regex VersionPattern = new(@"^vd{1,2}$", RegexOptions.Compiled);
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out var value) && value is string version)
{
return VersionPattern.IsMatch(version);
}
return false;
}
}
// Usage: /api/v1/users — /api/v99/users work, /api/latest does not
[Route("api/{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase { }
Example 3: Combining Multiple Constraints
// Stack multiple constraints on a single parameter
[HttpGet("products/{id:int:min(1):max(999999)}")]
public IActionResult GetProduct(int id)
{
// id is guaranteed: integer, >= 1, <= 999999
return View();
}
// Or with custom + built-in
[HttpGet("shop/{category:alpha:minlength(2)}/{slug:slug}")]
public IActionResult Product(string category, string slug)
{
// category: alphabetic, min 2 chars
// slug: matches custom slug pattern
return View();
}
5. Best Practices & Performance Guidelines
✅ DO
- Use
RegexOptions.Compiledfor constraints in the hot routing path - Keep constraint logic fast and stateless — no database calls
- Register constraints in
Program.cswith descriptive names - Combine with built-in constraints for layered validation
- Use
HashSetfor lookup-based constraints (O(1) performance)
❌ DON'T
- Don't make database calls inside constraints — move that to the controller
- Don't throw exceptions in
Match()— returnfalseinstead - Don't use constraints for authorization — that's for filters/middleware
- Don't create overly complex regex — it runs on every matching request
Production Pitfall: Database Lookups in Constraints
In a high-traffic application, constraints execute in the routing middleware pipeline — before controllers, before action filters. If you put a database query inside Match(), you create a bottleneck that runs on every incoming request, even for routes that don't match. Always validate existence (e.g., "does this blog slug exist in the database?") inside your controller action, not your constraint.
6. Interview Mastery
Q: "When would you create a custom route constraint instead of using model validation?"
Architect Answer: "Constraints and model validation serve different architectural layers. Constraints operate at the routing level — they determine whether a URL even matches a route. If the constraint fails, the route is skipped entirely and the next route is tried (or a 404 is returned). Model validation operates at the action level — the route has already matched, and we're validating the data payload. I use constraints for structural URL rules (slug format, API version, locale) and model validation for business data rules (email format, required fields, range checks)."