# 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 November 3, 2026

## 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:

```csharp
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:

```csharp
[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

- [Conventional and Attribute Routing](https://www.ask-codey.com/cheat-sheets/routing-in-aspnet-core-conventional-and-attribute-routing-cheat-sheet)

Source: https://www.ask-codey.com/lessons/routing-in-aspnet-core-conventional-and-attribute-routing
