← Back to Articles         
Custom Route Constraints

Custom Route Constraints

Published on 27 Sep 2026     5 min read ASP.NET Core MVC
MVC

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 IRouteConstraint interface.
  • Define the matching rule inside Match().
  • Register the constraint in ConstraintMap.
  • Use its registered name in the route: {id:even}.
  • Return true when the route matches; otherwise, return false.
  • 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:

  • Error is a fixed URL segment.
  • {statusCode} is a route parameter.
  • Its value is passed to the statusCode action 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 found
  • 403 – Access forbidden
  • 401 – Unauthorized
  • 500 – 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, 403 and 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.
← Back to Articles