Skip to content

Routing in ASP.NET Core MVC

When you type https://myshop.com/Products/Details/5 in the browser, how does ASP.NET Core know that it should run the Details method of ProductsController and pass 5 as the id? The answer is routing.

Routing is the process of looking at an incoming request's URL (and HTTP method) and deciding which code should handle it. In MVC, that code is a controller action. Routing also works the other way: it can generate URLs for your links from controller and action names.

Think of the reception desk of a big office building. A visitor says, "I'm here for the accounts department, invoice number 5." The receptionist checks the directory and sends the visitor to the right floor and the right person. Routing is that receptionist: the URL is the visitor's request, the route table is the directory, and the controller action is the person who finally helps.

How routing works in ASP.NET Core

Since ASP.NET Core 3.0, routing is called endpoint routing. An endpoint is anything that can handle a request: a controller action, a Razor Page, a minimal API (app.MapGet), a health check. Routing works in two steps inside the middleware pipeline:

  1. Matching (UseRouting): reads the URL, compares it with all registered routes, and picks the best endpoint. It also extracts route values such as controller = "Products", action = "Details", id = "5".
  2. Executing: at the end of the pipeline, the chosen endpoint runs (for MVC: the controller is created, model binding fills the action's parameters, the action runs).
Request: GET /Products/Details/5
   │
   ▼
UseHttpsRedirection, UseStaticFiles ...
   │
   ▼
UseRouting()            ──► matches the URL, selects the endpoint
   │                        route values: controller=Products, action=Details, id=5
   ▼
UseAuthentication()
UseAuthorization()      ──► can see WHICH endpoint was selected (e.g. its [Authorize])
   │
   ▼
Endpoint executes       ──► ProductsController.Details(int id = 5)
   │
   ▼
Response

Because the endpoint is selected before authorization runs, the authorization middleware can read the [Authorize] attribute of the exact action that is about to run. That's one of the main reasons endpoint routing was introduced.

A real example: this website's Program.cs

This is the routing code of the website you are reading right now, MakeMyCodes (simplified):

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();          // registers MVC (controllers + views)

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

// Conventional route for every page of the site
app.MapControllerRoute("default", "{controller=Dashboard}/{action=Index}");

// A special route: /sitemap.xml is handled by SitemapController.Index
app.MapControllerRoute(
    name: "sitemap",
    pattern: "sitemap.xml",
    defaults: new { controller = "Sitemap", action = "Index" });

app.Run();

The URL of this page is /NewMVCCrud/RoutingInAspNetCore. The default route reads it as controller = NewMVCCrud and action = RoutingInAspNetCore, so ASP.NET Core runs the RoutingInAspNetCore() method of NewMVCCrudController, which returns this view. The home page / has no segments, so the defaults are used: DashboardController.Index().

In .NET 6 and later, you don't have to call app.UseRouting() yourself. WebApplication adds it automatically at the start of the pipeline if you leave it out. Call it explicitly when you need middleware (like static files) to run before route matching.

Route templates: the language of routes

A route is described by a template: a pattern made of segments separated by /. Each segment is either fixed text or a parameter in curly braces.

Template partMeaningExample
productsLiteral: the URL must contain exactly this text (case does not matter)/products
{id}Parameter: captures any value into a route value named id/5 gives id = "5"
{id?}Optional parameter: the segment may be missingboth /Details and /Details/5
{controller=Home}Parameter with a default value used when the segment is missing/ gives controller = "Home"
{id:int}Parameter with a constraint: only matches if the value is an integer/5 matches, /abc does not
{*slug}Catch-all: captures the rest of the URL, including //docs/a/b/c gives slug = "a/b/c"

The two special parameter names {controller} and {action} tell MVC which controller class and which action method to run. The word "Controller" is left out of the URL: {controller} = Products means ProductsController.

Types of routing in ASP.NET Core MVC

MVC controllers support two types of routing:

Conventional routingAttribute routing
Where routes are definedIn one central place: Program.csOn controllers and actions, with attributes like [Route] and [HttpGet]
StyleOne pattern for many controllers ("convention")Each action declares its own URL
Best forMVC websites with many similar pagesWeb APIs, and pages that need custom, SEO-friendly URLs
Required for [ApiController]?No (not allowed)Yes

You can use both in the same application. Let's look at each one with examples.

1. Conventional routing

Conventional routing defines URL patterns once, in Program.cs, and every controller follows that convention.

The default route

The route most MVC projects start with:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

// Shortcut that registers exactly the same route:
app.MapDefaultControllerRoute();

How different URLs are matched by this one route:

URLcontrolleractionidMethod that runs
/Home (default)Index (default)(none)HomeController.Index()
/EmployeesEmployeesIndex (default)(none)EmployeesController.Index()
/Employees/Details/7EmployeesDetails7EmployeesController.Details(int id)
/employees/details/7employeesdetails7Same as above: matching is case-insensitive
/Employees/Details/7/extraToo many segmentsNo match: 404 Not Found
public class EmployeesController : Controller
{
    public IActionResult Index() => View();                    // /Employees

    public IActionResult Details(int id)                        // /Employees/Details/7
    {
        var employee = _repository.GetById(id);                 // id comes from the route
        return employee == null ? NotFound() : View(employee);
    }
}

The id route value is passed into the id parameter by model binding, because the names match.

Adding your own conventional routes

Suppose a blog wants URLs like /blog/2026/10/what-is-routing instead of /Blog/Post?year=2026&month=10&slug=what-is-routing. Add a custom route before the default one:

app.MapControllerRoute(
    name: "blogPost",
    pattern: "blog/{year:int}/{month:int}/{slug}",
    defaults: new { controller = "Blog", action = "Post" });

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
public class BlogController : Controller
{
    // /blog/2026/10/what-is-routing  ->  year=2026, month=10, slug="what-is-routing"
    public IActionResult Post(int year, int month, string slug)
    {
        var post = _blogService.Find(year, month, slug);
        return post == null ? NotFound() : View(post);
    }
}
  • blog is a literal, so this route only handles URLs that start with /blog/.
  • The pattern has no {controller} or {action}, so they come from defaults.
  • Register specific routes before general ones. When more than one conventional route could match a request, the one registered first wins.

Areas: routing for big applications

Large applications split features into areas, for example an Admin area with its own controllers and views (Areas/Admin/Controllers, Areas/Admin/Views). Areas need an extra route value:

app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
[Area("Admin")]
public class UsersController : Controller
{
    public IActionResult Index() => View();     // /Admin/Users
}

The :exists constraint means the first segment only matches if an area with that name really exists. That keeps /Employees going to the normal default route.

2. Attribute routing

With attribute routing, you write the URL directly on the controller and its actions. Each action says exactly which URL and which HTTP method it handles. You can read the URL right next to the code, without opening Program.cs.

If your app already calls MapControllerRoute (a normal MVC website), attribute routes work automatically. For an app with no conventional routes, such as a pure Web API, map the attribute-routed controllers in Program.cs:

app.MapControllers();        // maps all controllers that use attribute routes

Attribute routing in an MVC controller

[Route("employees")]                                  // prefix for every action in this controller
public class EmployeesController : Controller
{
    [HttpGet("")]                                     // GET /employees
    public IActionResult Index() => View();

    [HttpGet("{id:int}")]                             // GET /employees/7
    public IActionResult Details(int id) => View(_repository.GetById(id));

    [HttpGet("department/{name}")]                    // GET /employees/department/finance
    public IActionResult ByDepartment(string name) => View(_repository.GetByDepartment(name));

    [HttpGet("new")]                                  // GET  /employees/new   (shows the form)
    public IActionResult Create() => View();

    [HttpPost("new")]                                 // POST /employees/new   (saves the form)
    [ValidateAntiForgeryToken]
    public IActionResult Create(EmployeeForm form) { ... }
}
  • [Route("employees")] on the class is a prefix that every action's template is added to.
  • [HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete] set the URL and the HTTP method together.
  • Both Create methods share the URL /employees/new; the HTTP method (GET or POST) decides which one runs.
  • URLs no longer have to follow {controller}/{action}: /employees/department/finance is clean and readable.

Attribute routing in a Web API

Web APIs use attribute routing. In fact, a controller with [ApiController] must use attribute routes. A typical REST API:

[ApiController]
[Route("api/[controller]")]                           // [controller] is replaced by "Orders"
public class OrdersController : ControllerBase
{
    [HttpGet]                                         // GET    /api/Orders
    public IActionResult GetAll() => Ok(_orders.GetAll());

    [HttpGet("{id:int}")]                             // GET    /api/Orders/15
    public IActionResult Get(int id) => _orders.Find(id) is { } order ? Ok(order) : NotFound();

    [HttpPost]                                        // POST   /api/Orders
    public IActionResult Create(OrderDto dto)
    {
        var order = _orders.Add(dto);
        return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
    }

    [HttpPut("{id:int}")]                             // PUT    /api/Orders/15
    public IActionResult Update(int id, OrderDto dto) { ... }

    [HttpDelete("{id:int}")]                          // DELETE /api/Orders/15
    public IActionResult Delete(int id) { ... }

    [HttpGet("~/api/customers/{customerId:int}/orders")]   // ~/ ignores the controller prefix
    public IActionResult ForCustomer(int customerId) => Ok(_orders.ForCustomer(customerId));
}

Useful attribute routing features

FeatureExampleWhat it does
Token [controller][Route("api/[controller]")]Replaced by the controller name (without "Controller")
Token [action][Route("[controller]/[action]")]Replaced by the action method name
~/ prefix[HttpGet("~/about-us")]Ignores the controller-level prefix for this action
Several routes on one action[HttpGet("")] + [HttpGet("all")]The action is reachable from more than one URL
Name[HttpGet("{id}", Name = "GetOrder")]Gives the route a name for URL generation (CreatedAtRoute, Url.RouteUrl)
Order[Route("{slug}", Order = 2)]Lower order is tried first when routes would otherwise be ambiguous

Mixing both types: an action that has an attribute route is reachable only through its attribute route. Conventional routes are not used for it any more. This prevents the same page from being reachable under two different URLs by accident.

Route constraints

A constraint limits what a route parameter can match. It is written after a colon: {id:int}. If the value doesn't satisfy the constraint, that route simply doesn't match (and you get a 404 if nothing else matches).

ConstraintExampleMatches
int, long{id:int}123, -5
bool{active:bool}true, False
datetime{date:datetime}2026-10-29
decimal, double{price:decimal}49.99
guid{token:guid}3f2504e0-4f89-11d3-9a0c-0305e82c3301
min(n), max(n), range(a,b){page:range(1,500)}Integers in the range
minlength(n), maxlength(n), length(n){code:length(6)}Text of that length
alpha{name:alpha}Letters only (a-z, A-Z)
regex(...){pin:regex(^\d{{6}}$)}Text matching the regular expression

Constraints can be combined: {id:int:min(1)}. In a regex constraint inside a route template, curly braces are doubled ({{6}}) because single braces mean "parameter".

Important: constraints are for choosing the right route, not for validating user input. A wrong value gives a 404 ("page not found"), which is confusing for API clients that sent a bad id. Use constraints to tell routes apart (for example /products/5 vs /products/latest), and use model validation for input rules.

Generating URLs from routes

Routing also works in reverse: give it a controller, action and values, and it builds the URL. This means you never hard-code URLs. If a route changes later, every link updates automatically.

@* In a Razor view, using tag helpers *@
<a asp-controller="Employees" asp-action="Details" asp-route-id="7">View employee</a>
@* With the default conventional route this renders: href="/Employees/Details/7"   *@
@* With the attribute route [HttpGet("{id:int}")] it renders: href="/employees/7"  *@

<form asp-controller="Employees" asp-action="Create" method="post"> ... </form>
// In a controller
string url = Url.Action("Details", "Employees", new { id = 7 });   // "/Employees/Details/7"

return RedirectToAction("Details", new { id = employee.Id });       // redirect after saving
return CreatedAtAction(nameof(Get), new { id = order.Id }, order);   // 201 + Location header in APIs

Common routing problems and how to fix them

ProblemLikely reasonFix
404 for a URL you expect to workNo route template matches, a constraint failed, or the action has an attribute route so the conventional route is ignoredCheck the template, the constraint and the attributes on the action
AmbiguousMatchException: "The request matched multiple endpoints"Two actions have the same URL and HTTP methodMake the templates different, add a constraint, or use different HTTP verbs
An action ending in "Async" can't be foundMVC removes the "Async" suffix from action names by default (GetOrdersAsync becomes GetOrders)Use the name without "Async" in URLs and links, or set SuppressAsyncSuffixInActionNames = false
Custom route never usedA more general conventional route was registered first and matched the requestRegister specific routes before the default route

Routing in old ASP.NET Core versions

In ASP.NET Core 2.x you may see app.UseMvc(routes => routes.MapRoute(...)). That was the older, MVC-only routing system. From ASP.NET Core 3.0 it was replaced by endpoint routing (UseRouting + MapControllerRoute / MapControllers), which is shared by MVC, Razor Pages, minimal APIs, SignalR and health checks.

Interview questions

1. What is routing in ASP.NET Core?

Routing maps an incoming request URL (and HTTP method) to an endpoint, such as a controller action, and extracts route values like controller, action and id. It is also used to generate URLs from route values.

2. What are the types of routing in ASP.NET Core MVC?

Conventional routing, defined centrally in Program.cs with MapControllerRoute, and attribute routing, defined on controllers and actions with [Route], [HttpGet], etc. Both are built on endpoint routing.

3. What is the difference between UseRouting and the Map methods?

UseRouting adds the middleware that matches the request to an endpoint. MapControllerRoute, MapControllers, MapGet and others register the endpoints; the selected endpoint is executed at the end of the pipeline.

4. Why must UseAuthorization come after UseRouting?

Authorization needs to know which endpoint was selected, so it can read that action's [Authorize] or [AllowAnonymous] metadata. The endpoint is only known after routing has matched the request.

5. When would you use attribute routing instead of conventional routing?

For Web APIs (required with [ApiController]), for REST-style URLs that depend on the HTTP method, and when individual pages need custom or SEO-friendly URLs that don't follow {controller}/{action}.

6. What does {id?} mean, and what is {id:int}?

{id?} is an optional route parameter. {id:int} is a parameter with a constraint, so it only matches integer values.

7. What happens if an action has an attribute route and a conventional route also matches it?

The action is reachable only through its attribute route; conventional routes are not applied to actions that have attribute routes.

8. What is the difference between MapControllers and MapControllerRoute?

MapControllers() maps only attribute-routed controllers. MapControllerRoute() adds a conventional route (and also maps attribute-routed controllers). MapDefaultControllerRoute() is a shortcut for the standard {controller=Home}/{action=Index}/{id?} route.

Summary

  • Routing maps a URL to an endpoint (a controller action) and can also generate URLs.
  • Endpoint routing has two steps: UseRouting matches, and the endpoint runs at the end of the pipeline.
  • Route templates use literals, parameters {id}, optional parameters {id?}, defaults {controller=Home}, constraints {id:int} and catch-all {*slug}.
  • Conventional routing defines patterns centrally with MapControllerRoute; register specific routes first.
  • Attribute routing puts the URL on each action with [Route] and [HttpGet]; it is required for Web APIs.
  • Use constraints to choose routes, not to validate input; generate links with tag helpers and Url.Action instead of hard-coding URLs.

Related lessons: Custom Middleware, Filters in ASP.NET Core and Web API CRUD with Stored Procedures.