Introduction
Routing is how ASP.NET Core maps an incoming URL like /blog/2025/build-shopnest-with-aspnet-core to the correct controller action. Without routing, every request would hit one endpoint — your blog could never show individual posts, archives, or category pages.
ASP.NET Core supports two complementary styles: conventional routing (central templates in Program.cs) and attribute routing (routes declared on controllers and actions with [Route]). This lesson builds SEO-friendly blog URLs for ShopNest, covers constraints, areas, optional parameters, conflict resolution, and debugging — topics that appear in virtually every MVC interview.
After this article you will
- Configure conventional route templates with defaults and constraints
- Apply attribute routing with HTTP verb attributes
- Use route constraints (
int,regex, custom) - Set up area routing for admin vs public blog
- Design SEO-friendly slugs and debug route mismatches
Prerequisites
- Article 5 — Controllers and Actions
- ShopNest.Web MVC project with at least one controller
How routing works — the request pipeline
Level 1 — Analogy
Routing is like a postal sorting office. The envelope (HTTP request) has an address (URL path). The sorter reads the address, looks up rules (route table), and delivers the letter to the right department (controller) and desk (action).
Level 2 — Technical
After middleware runs, UseRouting() selects an endpoint. UseEndpoints() / MapControllerRoute registers MVC routes. The router produces a RouteData dictionary with keys like controller, action, id, plus custom tokens.
// Default conventional route (Program.cs)
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
URL /Posts/Show/42 → PostsController.Show(42). The ? on {id?} makes the segment optional.
Common misconceptions
❌ MYTH: You must choose conventional OR attribute routing.
✅ TRUTH: Both can coexist; attribute routes often take precedence when matched.
❌ MYTH: Route order never matters.
✅ TRUTH: First matching route wins — register specific routes before catch-all patterns.
Conventional routing — templates, defaults, constraints
Conventional routes are defined once and apply to all controllers following naming conventions.
// ShopNest Blog — SEO-friendly post URLs
app.MapControllerRoute(
name: "blogPost",
pattern: "blog/{year:int}/{month:int}/{slug}",
defaults: new { controller = "Posts", action = "Show" });
app.MapControllerRoute(
name: "blogArchive",
pattern: "blog/archive/{year:int}/{month:int?}",
defaults: new { controller = "Posts", action = "Archive" });
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
| Token | Meaning | Example |
|---|---|---|
{slug} | Captures any string | build-shopnest-with-aspnet-core |
{year:int} | Constraint — digits only | 2025 |
{month:int?} | Optional constrained segment | Omit for yearly archive |
defaults | Values when segment missing | controller = "Posts" |
Attribute routing
Place routes directly on controllers and actions — ideal for REST-style or versioned APIs and when URLs don't follow one global template.
[Route("blog")]
public class PostsController : Controller
{
[HttpGet("{year:int}/{month:int}/{slug}")]
public IActionResult Show(int year, int month, string slug) { ... }
[HttpGet("archive")]
[HttpGet("archive/{year:int}")]
public IActionResult Archive(int? year) { ... }
[HttpPost("create")]
public IActionResult Create([FromForm] PostCreateViewModel model) { ... }
}
Combine with HTTP verbs: [HttpGet], [HttpPost], [HttpPut], [HttpDelete]. Same path + different verbs = different actions (like REST).
Route constraints
// Built-in constraints
{id:int} // 123
{slug:minlength(3)} // at least 3 chars
{slug:regex(^[a-z0-9-]+$)} // lowercase slug only
{page:range(1,9999)}
// Custom constraint (implements IRouteConstraint)
{slug:slug} // register in Program.cs:
// options.ConstraintMap.Add("slug", typeof(SlugRouteConstraint));
Constraints reject invalid URLs early — /blog/abc/not-a-year/my-post returns 404 instead of hitting your action with bad data.
Area routing
Areas partition large apps — ShopNest Blog Admin vs public site.
// Areas/Admin/Controllers/PostsController.cs
[Area("Admin")]
[Route("admin/blog/[controller]")]
public class PostsController : Controller
{
[HttpGet("")]
public IActionResult Index() => View();
}
// Program.cs
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
Admin posts list: /Admin/Posts or /admin/blog/Posts depending on attribute prefix.
Conflicting routes — resolution
- Specific before general: Register
blog/{year}/{slug}before{controller}/{action}. - Ambiguous action: Two actions match same template — add constraints or rename routes.
- Link generation: Use named routes:
asp-route="blogPost"in Tag Helpers.
// Debugging: log matched endpoint (Development)
app.Use(async (ctx, next) =>
{
await next();
var ep = ctx.GetEndpoint();
if (ep != null) Console.WriteLine($"Endpoint: {ep.DisplayName}");
});
Install Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation and enable detailed errors in Development to see which route failed.
Hands-on — ShopNest Blog routing
- Add
Postmodel withSlug,PublishedAt. - Register
blogPostandblogArchiveroutes inProgram.cs. - Implement
PostsController.Showloading by year/month/slug. - Generate slugs on create:
title.ToLower().Replace(" ", "-")(or Slugify library). - Test: valid URL, wrong year (404), optional archive month.
public IActionResult Show(int year, int month, string slug)
{
var post = _db.Posts.FirstOrDefault(p =>
p.Slug == slug && p.PublishedAt.Year == year && p.PublishedAt.Month == month);
if (post == null) return NotFound();
return View(post);
}
Common errors & best practices
- 404 on valid URL: Route registered after default route — reorder in
Program.cs. - Optional parameter in middle: Not allowed — put optional segments at the end.
- SEO: Use readable slugs, lowercase, hyphens; include date in path for blogs if desired.
- Production: Prefer attribute routing for APIs; conventional for traditional MVC sites.
Interview questions
Q1: Conventional vs attribute routing?
A: Conventional uses central templates; attribute routes decorate controllers/actions — attribute gives fine-grained control per endpoint.
Q2: What does {id?} mean?
A: Optional route parameter — URL works with or without that segment.
Q3: How do route constraints help?
A: They validate segment format before model binding — invalid URLs never reach the action.
Q4: What is an Area?
A: Logical folder for controllers/views (e.g. Admin) with separate route prefix.
Q5: Which middleware enables routing?
A: UseRouting() selects endpoint; UseEndpoints/MapControllers executes it.
Summary
- Routing maps URLs to controller actions via conventional templates or attributes
- Use constraints and optional parameters for safe, flexible URLs
- Areas separate Admin from public ShopNest Blog
- Register specific routes first; debug with endpoint logging
Previous: Controllers and Actions
Next: Views and Razor Syntax
FAQ
Can I use both conventional and attribute routing in one app?
Yes. Many ShopNest-style apps use conventional routes for MVC pages and attribute routes for API controllers in the same project.
How do I generate links that match my custom blog route?
Use Tag Helpers: <a asp-route="blogPost" asp-route-year="2025" asp-route-month="5" asp-route-slug="my-post"> or Url.Action with route values.