Conventional and Attribute Routing

Conventional routing builds every address from one pattern; attribute routing gives each action its own address with [Route] and [HttpGet]. Constraints such as {id:int} decide which addresses match.

Journeyman · ASP.NET Core · C# · Updated

After this lesson you can

  • Give actions their own addresses with [Route], [HttpGet] and [HttpPost]
  • Use route constraints such as {id:int}, and explain how conventional and attribute routes differ

Key takeaways

  • Conventional routing builds every address from one pattern in Program.cs, such as {controller=Home}/{action=Index}/{id?}.
  • [Route("products")] on a controller starts every address in it; [HttpGet("...")] and [HttpPost("...")] on an action add the rest and say which method it answers.
  • A controller with attribute routes is reached only through them: its conventional address, such as /Products/Details/7, is 404 Not Found.
  • A constraint such as {id:int} matches only whole numbers, so /products/abc is 404 instead of reaching Details with an id of 0.
  • Choose attribute routes for friendly addresses such as /products/7, and for every Web API.

Routing is how ASP.NET Core turns an address into a controller action. You've used one way so far; there are two.

Conventional routing

Program.cs holds one pattern for the whole app:

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

Every controller's addresses follow it: /Products/Details/7, /Products/Search?name=sauce. Add an action, and it has an address at once. That's conventional routing: addresses by convention.

It's quick, but the addresses come from your class and method names, so /Products/Details/7 is what the world sees. Many sites prefer shorter, friendlier ones, such as /products/7.

Attribute routing

Attribute routing puts each address on the action itself:

[Route("products")]
public class ProductsController : Controller
{
    // GET /products
    [HttpGet("")]
    public IActionResult Index(int page = 1) { ... }

    // GET /products/7
    [HttpGet("{id:int}")]
    public IActionResult Details(int id) { ... }

    // GET /products/search?q=sauce
    [HttpGet("search")]
    public IActionResult Search([FromQuery(Name = "q")] string? text) { ... }

    // GET /products/new
    [HttpGet("new")]
    public IActionResult Create() { ... }

    // POST /products/new
    [HttpPost("new")]
    [ValidateAntiForgeryToken]
    public IActionResult Create(Product product) { ... }
}
  • [Route("products")] on the class is the start of every address in it.
  • [HttpGet("...")] and [HttpPost("...")] on an action add the rest, and say which method it answers.
  • {id:int} is a route parameter, filled into the id parameter, as with conventional routing.

The two Create actions share the address /products/new: the GET shows the form, the POST saves it.

Tag helpers follow the attribute routes, with no changes to your views: asp-action="Details" asp-route-id="7" now writes href="/products/7".

Once a controller has attribute routes

A controller with attribute routes is reached only through them. /Products/Details/7 no longer works for this controller: it's 404 Not Found. Other controllers, such as HomeController, keep their conventional addresses. A site can mix the two, controller by controller.

Route constraints

The :int in {id:int} is a constraint: the route only matches when that part is a whole number.

Address Result
/products/7 Details(7)
/products/abc 404: abc isn't an int, so no route matches
/products/search Search: a fixed word in a template always beats a {parameter}

That's why constraints matter: without :int, /products/abc would still reach Details, with an id it couldn't fill (0). (/products/search is safe either way: a fixed word always beats a {parameter}.)

Constraint Matches
{id:int} Whole numbers
{id:int:min(1)} Whole numbers from 1 up
{slug:alpha} Letters only
{date:datetime} Dates, such as 2026-03-06
{id?} Optional: the part may be left out

Which to use

Conventional Attribute
Addresses come from One pattern in Program.cs Each action's own attributes
Good for Quick sites where class names make fine addresses Chosen, friendly addresses; and Web APIs, which always use attribute routes
A new action Has an address at once Needs its own [HttpGet] or [HttpPost]

Check yourself

2 questions to try on the lesson page.

So what

Routes are the front door of every action. Conventional routes come for free; attribute routes let you choose addresses people can read and remember, and you'll use them for every Web API you build.

Key terms

  • Conventional Routing
  • Attribute Routing
  • Route Template
  • Route Constraint

Cheat sheets