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?}");
/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.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.{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.