TL;DR
- Route constraints filter URL parameters before they reach your controller actions
- The syntax
{parameterName:constraintName}
works identically in both controller routes and minimal APIs - ASP.NET Core includes over 20 built-in constraints (
int
,guid
,bool
, etc.) for quick parameter validation - Constraints can be chained together for precise validation:
{id:int:min(1):max(100)}
- Using constraints properly prevents ambiguous routes and route conflicts
- Constraints generate clean 404 responses for invalid URLs rather than application errors
- Strategic constraint use creates self-documenting APIs that clearly define valid input formats
- Real-world examples show how constraints solve common API design challenges
Ever created a route that caught everything except what you wanted? Or had two routes fighting over the same URL? Route constraints in ASP.NET Core fix this problem, they’re like bouncers at a club, only letting the right requests through to your endpoints.
Understanding Route Constraints
What Are Route Constraints and Why Use Them?
Route constraints work like filters at a checkpoint, only matching requests get through. They check URL parameters before your controller code runs, which prevents confusion and makes your API more reliable.
Without constraints, a route like /orders/{id}
would match both /orders/123
and /orders/abc123
. With constraints, you can make sure only real order IDs (like numbers) reach your actual code.
How Route Constraints Work
When ASP.NET Core processes a request:
- It extracts the URL path
- It compares the path against all defined routes
- For each potential route match, it evaluates any constraints
- If a constraint fails, that route is eliminated as a candidate
- The first route that matches with all constraints passing gets used
Constraints are evaluated in a specific order: type constraints first (like int
), then value constraints (like range
), then string constraints (like length
), and finally custom and regex constraints.
Using Built-in Route Constraints
ASP.NET Core 8 includes many built-in constraints. Here’s how to use them:
Controller-Based Routing
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
// Only matches integer IDs like /api/orders/123
[HttpGet("{id:int}")]
public IActionResult GetOrder(int id)
{
return Ok($"Order {id}");
}
// Only matches GUID references like /api/orders/550e8400-e29b-41d4-a716-446655440000
[HttpGet("ref/{reference:guid}")]
public IActionResult GetOrderByReference(Guid reference)
{
return Ok($"Order reference {reference}");
}
// Username must be 3-20 characters
[HttpGet("user/{username:length(3,20)}")]
public IActionResult GetUserOrders(string username)
{
return Ok($"Orders for {username}");
}
}
Minimal APIs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Integer constraint for product IDs
app.MapGet("/products/{id:int}", (int id) =>
Results.Ok($"Product {id}"));
// Range constraint for pagination
app.MapGet("/products/page/{page:range(1,100)}", (int page) =>
Results.Ok($"Page {page}"));
// Boolean constraint for feature flags
app.MapGet("/features/{enabled:bool}", (bool enabled) =>
Results.Ok($"Feature is {(enabled ? "enabled" : "disabled")}"));
app.Run();
Route Constraint Syntax and Options
Basic Constraint Syntax
The basic syntax for adding a constraint to a route parameter is:
{parameterName:constraintName}
For constraints that take arguments, use this format:
{parameterName:constraintName(arg1,arg2)}
Combining Multiple Constraints
You can chain multiple constraints using additional colons:
{id:int:min(1):max(100)}
All constraints must pass for the route to match.
Complete Route Constraints Reference
Constraint | Syntax | Example | Matches |
---|---|---|---|
alpha | {name:alpha} | /category/books | Alphabetic characters only (a-zA-Z) |
alphanum | {code:alphanum} | /product/abc123 | Alphanumeric characters only (a-zA-Z0-9) |
bool | {flag:bool} | /features/enabled/true | Boolean values (true/false) |
datetime | {date:datetime} | /events/2023-05-25 | Date/time values |
decimal | {price:decimal} | /products/price/19.99 | Decimal values |
double | {value:double} | /readings/3.14159 | Double-precision floating point values |
{contact:email} | /users/info@example.com | Email addresses | |
file | {document:file} | /docs/report.pdf | File names with extension |
float | {value:float} | /metrics/98.6 | Single-precision floating point values |
guid | {id:guid} | /orders/550e8400-e29b-41d4-a716-446655440000 | GUID/UUID format |
int | {id:int} | /users/123 | Integer values (32-bit) |
length | {name:length(3,10)} | /users/john | String length within specified range |
long | {id:long} | /records/9223372036854775807 | Long integer values (64-bit) |
max | {value:max(100)} | /quantity/50 | Values less than or equal to specified maximum |
maxlength | {desc:maxlength(50)} | /posts/short-description | Strings with length less than or equal to max |
min | {value:min(1)} | /quantity/5 | Values greater than or equal to specified minimum |
minlength | {name:minlength(3)} | /users/bob | Strings with length greater than or equal to min |
range | {page:range(1,100)} | /page/5 | Values within specified numeric range |
regex | {code:regex(^[A-Z]{{2}}$)} | /country/US | Custom pattern using regular expression |
required | {name:required} | /users/john | Non-empty values |
Advanced Route Constraint Techniques
Going Beyond Built-in Constraints
When you need more specialized validation, you have two options:
Option 1: Regex Constraints for Simple Patterns
For straightforward pattern matching, the built-in regex constraint is often sufficient:
// Match product codes like "PRD-1234"
[HttpGet("products/{code:regex(^PRD-\\d{{4}}$)}")]
public IActionResult GetProduct(string code)
{
return Ok($"Product: {code}");
}
Option 2: Custom IRouteConstraint Implementation
For complex validation logic or database lookups, you’ll need a full custom constraint implementation. Check out our dedicated guide for a step-by-step walkthrough.
Practical Applications of Route Constraints
Route Constraints for Error Prevention
One of the best uses of route constraints is preventing common API usage errors. Let’s see how:
Preventing 404s with Fallback Routes
Structure your routes with increasingly specific constraints and a fallback:
[Route("api/products")]
public class ProductsController : ControllerBase
{
// Most specific - matches numeric IDs
[HttpGet("{id:int}")]
public IActionResult GetById(int id) => Ok($"Product {id}");
// Less specific - matches SKUs like "ABC-123"
[HttpGet("{sku:regex(^[A-Z]{{3}}-\\d{{3}}$)}")]
public IActionResult GetBySku(string sku) => Ok($"Product SKU: {sku}");
// Fallback - matches other valid strings
[HttpGet("{slug:minlength(3):maxlength(50)}")]
public IActionResult GetBySlug(string slug) => Ok($"Product: {slug}");
// Catch-all for invalid patterns
[HttpGet("{*catchAll}")]
public IActionResult HandleInvalid() => BadRequest("Invalid product identifier");
}
This approach creates a hierarchy of constraints that ensures users get helpful responses instead of 404 errors.
Real-World Route Constraint Scenarios
Let’s explore some practical examples of how route constraints solve common API design challenges:
Versioned API Endpoints
Use constraints to route requests to different API versions:
// API Version 1
[HttpGet("api/v{version:int:min(1):max(1)}/users")]
public IActionResult GetUsersV1()
{
return Ok("Version 1 API");
}
// API Version 2
[HttpGet("api/v{version:int:min(2):max(2)}/users")]
public IActionResult GetUsersV2()
{
return Ok("Version 2 API with enhanced features");
}
Multi-tenant Applications
Route to different tenants based on subdomain or path:
// Match tenant-specific routes
[HttpGet("{tenant:regex(^(?!www)[a-z0-9]+$)}/dashboard")]
public IActionResult TenantDashboard(string tenant)
{
return Ok($"Dashboard for tenant: {tenant}");
}
SEO-friendly Blog URLs
Create clean, readable URLs for blog posts:
// Match blog post URLs with year/month/slug pattern
// Example: /blog/2025/07/understanding-route-constraints
app.MapGet("/blog/{year:int:min(2000):max(2100)}/{month:range(1,12)}/{slug:minlength(5)}",
(int year, int month, string slug) =>
{
return Results.Ok($"Blog post from {year}/{month:D2}: {slug}");
});
Resource Ownership Patterns
Define routes that clearly indicate resource ownership:
// /users/123/documents/456 - Document 456 belongs to User 123
[HttpGet("users/{userId:int}/documents/{docId:guid}")]
public IActionResult GetUserDocument(int userId, Guid docId)
{
return Ok($"Document {docId} for user {userId}");
}
These patterns demonstrate how route constraints can create intuitive, self-documenting APIs that clearly communicate what resources are accessible and in what format parameters should be provided.
Best Practices and Considerations
Common Pitfalls to Avoid
Route Ordering Matters: More specific routes should come before generic ones. Without constraints, /orders/summary
might match a generic {id}
parameter.
Missing Constraint Matches: If no route constraint matches, you’ll get a 404. Always have fallback routes or handle edge cases.
Over-constraining: Don’t make constraints so strict that valid requests get rejected. Balance validation with usability.
When to Use Route Constraints vs. Other Validation Approaches
Understanding when to use route constraints versus other validation approaches is crucial for good API design:
Validation Approach | Best For | Example |
---|---|---|
Route Constraints | URL structure and format validation | /users/{id:int} |
Model Validation | Complex business rules and relationships | [Range(1, 100)] public int Quantity { get; set; } |
Manual Validation | Dynamic rules that depend on application state | if (userId != currentUser.Id) return Forbid(); |
Middleware | Cross-cutting concerns affecting multiple routes | Custom authorization middleware |
Route constraints are ideal for ensuring URL parameters match expected formats, while deeper validation logic belongs in your controller actions or service layer.
Related Posts
- Routing in ASP.NET Core: A Comprehensive Guide
- Dependency Inversion Principle in C#: Flexible Code with ASP.NET Core DI
- Handling Request Cancellation in ASP.NET Core: From Browser to Database
- Custom routing constraint in AAP.NET core
- C# Data Annotations: Complete Guide with Examples, Validation, and Best Practices