Custom Route Constraints
A custom route constraint controls whether a route parameter matches a specific rule.
Example: Allow only even product IDs.
1. Create the Constraint
using Microsoft.AspNetCore.Routing;
public class EvenNumberConstraint : IRouteConstraint
{
public bool Match(
HttpContext? httpContext,
IRouter? route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
return values.TryGetValue(routeKey, out var value)
&& int.TryParse(value?.ToString(), out int number)
&& number % 2 == 0;
}
}
2. Register in Program.cs
builder.Services.AddRouting(options =>
{
options.ConstraintMap.Add(
"even",
typeof(EvenNumberConstraint));
});
3. Use the Constraint
[HttpGet("products/{id:even}")]
public IActionResult Details(int id)
{
return View();
}
| URL | Result |
|---|---|
/products/10 |
Route matches |
/products/11 |
Route does not match—returns 404 |
/products/abc |
Route does not match—returns 404 |
Key Points
- Implement the
IRouteConstraintinterface. - Define the matching rule inside
Match(). - Register the constraint in
ConstraintMap. - Use its registered name in the route:
{id:even}. - Return
truewhen the route matches; otherwise, returnfalse. - Route constraints help select an endpoint; they should not be used for business validation.
Handling Invalid or Unmatched Routes
When no route matches a request, ASP.NET Core normally returns 404 Not Found.
1. Create a Catch-All Route
Place it after all other routes:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "fallback",
pattern: "{*url}",
defaults: new
{
controller = "Error",
action = "NotFound"
});
2. Create the Error Controller
public class ErrorController : Controller
{
public IActionResult NotFound()
{
Response.StatusCode = StatusCodes.Status404NotFound;
return View();
}
}
Create the corresponding view:
Views/Error/NotFound.cshtml
3. Handle Other Status Codes
Configure status-code pages in Program.cs:
app.UseStatusCodePagesWithReExecute(
"/Error/{0}");
public class ErrorController : Controller
{
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
{
Response.StatusCode = statusCode;
return statusCode == 404
? View("NotFound")
: View("Error");
}
}
Key Points
- An unmatched route normally produces 404 Not Found.
- Use a catch-all parameter such as
{*url}to handle unmatched URLs. - Register the catch-all route after specific routes.
- Preserve the correct HTTP status code instead of returning
200 OK. UseStatusCodePagesWithReExecute()internally executes an error endpoint while preserving the original URL.- Use
UseExceptionHandler()separately for unhandled exceptions; exceptions and unmatched routes are different.
Meaning of [Route("Error/{statusCode}")]
It defines the URL pattern used to call the action.
[Route("Error/{statusCode}")]
Here:
Erroris a fixed URL segment.{statusCode}is a route parameter.- Its value is passed to the
statusCodeaction parameter.
Example
When the URL is:
/Error/404
ASP.NET Core calls:
public IActionResult HttpStatusCodeHandler(int statusCode)
The value becomes:
statusCode = 404
Complete Example
In Program.cs:
app.UseStatusCodePagesWithReExecute("/Error/{0}");
Controller:
public class ErrorController : Controller
{
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
{
if (statusCode == 404)
{
return View("NotFound");
}
return View("Error");
}
}
How {0} and {statusCode} Connect
Page not found
↓
Status code = 404
↓
"/Error/{0}" becomes "/Error/404"
↓
[Route("Error/{statusCode}")]
↓
statusCode = 404
| Code | Meaning |
|---|---|
{0} |
Placeholder replaced with the actual status code |
/Error/404 |
Internally generated error path |
{statusCode} |
Captures 404 from the URL |
int statusCode |
Receives the captured value |
Key Point
The names must match:
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
Alternatively, you can use:
[Route("Error/{code}")]
public IActionResult HttpStatusCodeHandler(int code)
Both are valid because the route parameter and method parameter use the same name.
What is UseStatusCodePagesWithReExecute()?
UseStatusCodePagesWithReExecute() is ASP.NET Core middleware used to display a custom page for HTTP error status codes such as:
404– Page not found403– Access forbidden401– Unauthorized500– Internal server error, when explicitly returned
Configuration
Add it to Program.cs before route mapping:
app.UseStatusCodePagesWithReExecute("/Error/{0}");
{0} is replaced with the actual HTTP status code.
404 → /Error/404
403 → /Error/403
Controller
public class ErrorController : Controller
{
[Route("Error/{statusCode}")]
public IActionResult HandleStatusCode(int statusCode)
{
ViewBag.StatusCode = statusCode;
return statusCode switch
{
404 => View("NotFound"),
403 => View("Forbidden"),
_ => View("Error")
};
}
}
How It Works
User requests /wrong-page
↓
No matching endpoint
↓
Status code becomes 404
↓
Internally re-executes /Error/404
↓
ErrorController.HandleStatusCode(404)
↓
Displays NotFound.cshtml
The browser URL remains:
/wrong-page
Re-Execute vs Redirect
WithReExecute |
WithRedirects |
|---|---|
| Internally runs another endpoint | Sends a redirect to the browser |
| Original browser URL remains | Browser URL changes |
| Usually keeps the original status code | Redirect response occurs first |
| No additional browser request | Browser makes another request |
Key Points
- It handles responses with status codes from 400 to 599 that do not already have a response body.
- It internally executes another route to generate a custom error page.
{0}represents the original status code.- It is mainly used for custom
404,403and similar pages. - It does not directly catch exceptions; use
UseExceptionHandler()for unhandled exceptions.
UseStatusCodePagesWithReExecute() is ASP.NET Core middleware, so it is not limited to MVC applications.
| Application type | Can use it? | Typical error endpoint |
|---|---|---|
| ASP.NET Core MVC | Yes | Controller action and Razor View |
| Razor Pages | Yes | Razor error page |
| Web API | Technically yes | API endpoint |
| Minimal API | Technically yes | Minimal API handler |
MVC Example
app.UseStatusCodePagesWithReExecute("/Error/{0}");
It re-executes an MVC action:
[Route("Error/{statusCode}")]
public IActionResult HandleStatusCode(int statusCode)
{
return View("Error");
}
Web API Recommendation
For Web APIs, returning standardized JSON using ProblemDetails is usually more appropriate than re-executing an HTML error page:
app.UseStatusCodePages(async context =>
{
var response = context.HttpContext.Response;
await response.WriteAsJsonAsync(new
{
status = response.StatusCode,
message = "The requested resource was not found."
});
});
Key Points
- It belongs to the general ASP.NET Core middleware pipeline.
- It can work with MVC, Razor Pages, Web APIs and Minimal APIs.
- It is most commonly used in MVC and Razor Pages for custom HTML error pages.
- For APIs, prefer structured JSON or
ProblemDetails. - Use
UseExceptionHandler()for unhandled exceptions.