Introduction to ASP.NET Core Routing
At its core, routing in ASP.NET Core is just about connecting HTTP requests to the right code in your application. Good routing gives you clean, readable URLs that make sense to both users and search engines.
Since ASP.NET Core 3.0, we’ve been using something called Endpoint Routing, which has gotten better with each new version. It works the same way across all parts of your app, whether you’re using MVC controllers, Razor Pages, or the newer minimal APIs.
There are two main ways to set up your routes:
- Attribute Routing: You put route information right on your controllers and methods using attributes like
[Route]
and[HttpGet]
. - Conventional Routing: You define your routes in one central place in your startup code, setting patterns that apply across your application.
Let’s look at both and see how routing actually works behind the scenes.
How ASP.NET Core Routing Works
When someone visits your site, here’s what happens with routing:
- The system grabs the URL path from the incoming request
- It checks this path against all your defined routes
- It picks the best match based on your rules and constraints
- It pulls out any parameters from the URL (like IDs or names)
- It sends the request to the right piece of code (usually a controller action)
If it can’t find any matching route, your visitor gets a 404 Not Found error.
Attribute Routing in ASP.NET Core
In ASP.NET Core, routing is the process of matching a request URL to a specific action method in a controller. It is an important part of the application’s architecture and is used to determine how the application should respond to a request.
To define routes in an application, we can use the Route
attribute on an action method in a controller, or we can define routes in the application’s startup configuration.
When a request is received by the application, the routing system examines the request URL and compares it to the defined routes.
If a match is found, the request is forwarded to the corresponding action method. If no match is found, the routing system returns a 404 error.
Attribute Routing in Detail
With attribute routing, you stick your route definitions right on your controllers and actions using C# attributes. I like this approach because you can see exactly what URLs map to what code just by looking at your controller.
Controller-Level Routes
You can add a [Route]
attribute to your controller to set up a base path for everything inside it:
[Route("api/products")]
public class ProductsController : ControllerBase
{
// All action methods inherit this route prefix
}
Action-Level Routes
Each method can also have its own route, which gets combined with the controller’s route:
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet] // GET /api/products
public IActionResult GetAll()
{ /* ... */ }
[HttpGet("{id:int}")] // GET /api/products/123
public IActionResult GetById(int id)
{ /* ... */ }
[HttpPost] // POST /api/products
public IActionResult Create([FromBody] Product product)
{ /* ... */ }
[HttpPut("{id:int}")] // PUT /api/products/123
public IActionResult Update(int id, [FromBody] Product product)
{ /* ... */ }
[HttpDelete("{id:int}")] // DELETE /api/products/123
public IActionResult Delete(int id)
{ /* ... */ }
}
Route Parameters
The system automatically pulls values from the URL and puts them into your method parameters:
[HttpGet("categories/{categoryId}/products/{productId}")]
public IActionResult GetProduct(int categoryId, int productId)
// These values come straight from the URL -> no parsing needed
return Ok($"Category: {categoryId}, Product: {productId}");
}
Route Template Tokens
ASP.NET Core has some special placeholders you can use in your routes:
[controller]
: Gets replaced with your controller name (without the “Controller” part)[action]
: Gets replaced with the method name[area]
: Gets replaced with the area name (if you’re using areas)
[Route("api/[controller]")] // Becomes "api/products"
public class ProductsController : ControllerBase
{
[HttpGet("[action]/{id}")] // GET /api/products/details/123
public IActionResult Details(int id)
{ /* ... */ }
}
Conventional Routing in ASP.NET Core
While attribute routing works great for APIs, conventional routing gives you a central place to define your URL patterns. This works really well for MVC apps where you want similar URL structures across all your controllers.
Setting Up Conventional Routes
In newer ASP.NET Core apps (.NET 8+), you set up conventional routes in your Program.cs
file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
That pattern might look strange, but here’s what it means:
{controller=Home}
: Which controller to use (if not specified, use “Home”){action=Index}
: Which method to call (if not specified, use “Index”){id?}
: An optional parameter named “id”
How Conventional Routes Work
With this default pattern, here’s how different URLs get handled:
URL | Controller | Action | Parameters |
---|---|---|---|
/ | HomeController | Index | None |
/Products | ProductsController | Index | None |
/Products/Details | ProductsController | Details | None |
/Products/Details/5 | ProductsController | Details | id = 5 |
Multiple Route Patterns
You can set up several different route patterns for different parts of your site:
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "blog",
pattern: "blog/{year}/{month}/{day}/{slug}",
defaults: new { controller = "Blog", action = "Post" },
constraints: new { year = @"\d{4}", month = @"\d{2}", day = @"\d{2}" });
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
The system checks routes in the order you define them, so always put your more specific routes first.
Route Constraints
Route constraints let you check that URL parameters match the format you expect. They filter out bad requests before they even reach your code.
How Route Constraints Work
To use constraints, add them after a colon in your parameter like this: {parameter:constraint}
. The constraint is basically a rule that the parameter must follow.
Here’s how you’d make sure an ID is actually a number:
[Route("api/products/{id:int}")]
public IActionResult GetProduct(int id)
{
// This only runs when id is actually a number
return Ok($"Product ID: {id}");
}
If someone tries to access /api/products/abc
, this route won’t match because “abc” isn’t a number. The system will keep looking for another matching route or just return a 404.
Built-in Route Constraints
ASP.NET Core comes with lots of constraints ready to use:
Constraint | What it checks | Example |
---|---|---|
int | Is it a whole number? | {id:int} |
bool | Is it true/false? | {active:bool} |
datetime | Is it a valid date and time? | {date:datetime} |
decimal | Is it a decimal number? | {price:decimal} |
double | Is it a floating point number? | {rate:double} |
float | Is it a floating point number? | {value:float} |
guid | Is it a GUID? | {id:guid} |
long | Is it a big whole number? | {count:long} |
minlength(value) | Is it at least this long? | {name:minlength(4)} |
maxlength(value) | Is it no longer than this? | {name:maxlength(20)} |
length(min,max) | Is it between these lengths? | {name:length(4,20)} |
min(value) | Is the number at least this big? | {age:min(18)} |
max(value) | Is the number no bigger than this? | {age:max(120)} |
range(min,max) | Is the number in this range? | {age:range(18,65)} |
alpha | Does it contain only letters? | {name:alpha} |
regex(pattern) | Does it match this pattern? | {sku:regex(^[A-Z]{3}\\d{4}$)} |
required | Is there something here? | {id:required} |
Combining Multiple Constraints
You can stack constraints by separating them with colons:
[HttpGet("users/{username:alpha:minlength(4):maxlength(20)}")]
public IActionResult GetUser(string username)
{
// This only runs if username has only letters and is 4-20 characters long
return Ok($"User: {username}");
}
Custom Route Constraints
Need something more specific? You can create your own custom constraints by implementing the IRouteConstraint
interface.
This comes in handy when you need to check against business rules or patterns that the built-in constraints don’t cover.
Route Value Transformers
Route value transformers let you process URL segments before they’re matched to your code. ASP.NET Core has a built-in SlugifyParameterTransformer
that converts segments to kebab-case (with hyphens):
// Add this in Program.cs or Startup.cs
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
// Then use it in your routes
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify}/{action:slugify}/{id?}");
With this set up, a URL like /product-catalog/product-details/5
would map to ProductCatalogController.ProductDetails(5)
. Pretty neat, right?
URL Generation
ASP.NET Core makes it easy to create URLs based on your routes:
Using IUrlHelper in Controllers
// Generate a URL to an action in the same controller
public IActionResult SomeAction()
{
// Creates: /Products/Details/5
string url = Url.Action("Details", new { id = 5 });
return Ok($"Generated URL: {url}");
}
// Generate a URL to an action in another controller
public IActionResult LinkToOrder()
{
// Creates: /Orders/View/10
string url = Url.Action("View", "Orders", new { id = 10 });
return Ok($"Order URL: {url}");
}
Using Tag Helpers in Razor Views
<!-- Makes a link to ProductController.Details(5) -->
<a asp-controller="Product" asp-action="Details" asp-route-id="5"
>View Product</a
>
<!-- Makes a form that posts to the current controller's Create action -->
<form asp-action="Create" method="post">
<!-- Form fields -->
<button type="submit">Create</button>
</form>
Route Precedence
When several routes could match a URL, how does ASP.NET Core decide? It follows these rules:
- Routes with more exact text segments win
- Exact text beats parameters (like
/products
beats/{controller}
) - Routes with constraints beat those without
- Catch-all parameters (like
{**all}
) come last
Knowing these rules will save you a ton of headaches when debugging routing problems.
Best Practices for ASP.NET Core Routing
Design Clean URLs
- Use nouns for things (like
/products
instead of/getProducts
) - Put related resources in a hierarchy (like
/customers/5/orders
) - Use hyphens for multi-word paths (like
/product-categories
) - Avoid query strings for main resources (better to use
/orders/pending
than/orders?status=pending
)
Route Organization
- Group similar endpoints under a common prefix
- Follow REST patterns for APIs
- Use areas to split up big apps
- Include versions in your API routes (like
/api/v1/products
)
Security Considerations
- Always check route parameters before using them in database queries
- Use constraints to make sure parameters match expected formats
- Keep sensitive info out of URLs
- Think about adding rate limits to public endpoints
Conclusion
ASP.NET Core’s routing system gives you a lot of flexibility in how you map URLs to your code. Whether you like attribute routing for its directness or conventional routing for its consistency, knowing how routing works will help you build better web apps.
Route constraints are super useful for checking parameters right in the URL pattern. This means bad requests get stopped before they even reach your code. When you combine this with good URL design and the right tools for generating links, you can create an app that’s easy to navigate.
If you want to go deeper, check out making your own custom constraints, tweaking the routing middleware, or connecting routing with other ASP.NET Core features like authorization.