Conventional Routing — Route Templates and Constraints

Routing connects incoming URLs to controller actions. ASP.NET Core MVC supports two routing strategies: conventional routing, where URL patterns are defined centrally in Program.cs and map to controllers and actions by convention; and attribute routing, where route templates are defined directly on controllers and actions with [Route] and [HttpGet] attributes. Conventional routing is the traditional MVC approach suited to site-style applications; attribute routing is preferred for APIs and complex URL structures. Both can coexist in the same application.

Conventional Routing

// ── Single default route — covers most MVC applications ───────────────────
app.MapControllerRoute(
    name:    "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

// Pattern breakdown:
// {controller=Home} — matches any segment; defaults to "Home" if missing
// {action=Index}    — matches any segment; defaults to "Index" if missing
// {id?}             — optional segment; null if not present

// URL examples:
// /                      → HomeController.Index()
// /home                  → HomeController.Index()
// /posts                 → PostsController.Index()
// /posts/details         → PostsController.Details() with id=null
// /posts/details/42      → PostsController.Details() with id=42

// ── Multiple named routes ──────────────────────────────────────────────────
// Evaluated in registration order — first match wins
app.MapControllerRoute(
    name:    "blog",
    pattern: "blog/{year}/{month}/{slug}",
    defaults: new { controller = "Blog", action = "Post" },
    constraints: new { year = @"\d{4}", month = @"\d{2}" });

app.MapControllerRoute(
    name:    "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
Note: Routes are evaluated in registration order — the first matching route wins. Put more specific routes before more general ones. If you register the default route first, it matches /blog/2025/01/my-post as controller=blog, action=2025 — not what you want. Always register specific routes before the catch-all default route. This is the most common routing bug in MVC applications: a specific URL is matched by the default route instead of a custom one because the custom route was registered too late.
Tip: Use Url.Action() and RedirectToAction() for link generation — never hardcode URLs. When you rename a controller or action, hardcoded URLs become broken links instantly. Generated URLs from Url.Action("Details", "Posts", new { id = post.Id }) or Tag Helper asp-controller="Posts" asp-action="Details" asp-route-id="@post.Id" automatically update when the route changes. Link generation is one of the most valuable features of the routing system.
Warning: Route constraints restrict which values a route segment matches — they do not perform input validation. A constraint like {id:int} means “only match if this segment is an integer” — it returns 404 (no match) for non-integer segments, not 400 Bad Request. Use constraints for disambiguation (route A handles integer IDs, route B handles string slugs) and use model binding validation for security and business rule validation.

Route Constraints

// ── Built-in constraints ───────────────────────────────────────────────────
// {id:int}              — must be an integer
// {id:long}             — must be a long integer
// {id:guid}             — must be a GUID
// {id:bool}             — must be true or false
// {id:minlength(5)}     — minimum 5 character string
// {id:maxlength(20)}    — maximum 20 character string
// {id:min(1)}           — integer minimum value
// {id:max(100)}         — integer maximum value
// {id:range(1,100)}     — integer in range
// {slug:regex(^[a-z0-9-]+$)}  — must match regex
// {id:alpha}            — alphabetic characters only

// ── Attribute routing (works alongside conventional routing) ───────────────
[Route("posts")]
public class PostsController : Controller
{
    [Route("")]           // GET /posts
    [Route("index")]      // GET /posts/index
    public async Task<IActionResult> Index() { /* ... */ return View(); }

    [Route("{id:int}")]           // GET /posts/42
    public async Task<IActionResult> Details(int id) { /* ... */ return View(); }

    [Route("{slug:regex(^[a-z0-9-]+$)}")]  // GET /posts/my-article-title
    public async Task<IActionResult> BySlug(string slug) { /* ... */ return View(); }
}

// ── Link generation using named route ─────────────────────────────────────
// In a view:
// <a asp-route="blog" asp-route-year="2025" asp-route-month="01"
//    asp-route-slug="my-post">Read More</a>
// Generates: /blog/2025/01/my-post

Common Mistakes

Mistake 1 — Registering specific routes after the default route (never matched)

❌ Wrong — default route matches everything first:

app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute("blog", "blog/{year}/{month}/{slug}", ...);  // never reached!

✅ Correct — register specific routes first, default route last.

Mistake 2 — Using route constraints for security validation

❌ Wrong — relying on {id:int} to block malicious non-integer input (returns 404, not validation error).

✅ Correct — use constraints for routing disambiguation; validate for security in model binding and service layer.

🧠 Test Yourself

Two routes are registered: “blog” matching blog/{year:int}/{slug} and “default” matching {controller}/{action}/{id?}. A request arrives for /blog/2025/my-post. Which route matches?