Routing
Key Points
- Endpoint routing is the modern model:
UseRouting()matches the URL to an endpoint;UseEndpoints()(or implicitMap*calls) executes it. - Route templates like
/orders/{id:int}use constraints (int,guid,min(1),regex(...)). - Catch-all (
{**slug}) captures multi-segment paths. - Order matters: more specific routes should come first; catch-alls last. The framework picks the most specific match.
- Reverse routing via
LinkGenerator/Url.Action/Url.Pageproduces URLs from route names + values. - Route groups (
MapGroup) inherit metadata; nesting allowed.
Concepts (deep dive)
Endpoint routing pipeline
UseRouting() doesn't execute — it just sets HttpContext.Endpoint. Anything between UseRouting and UseEndpoints (or terminal Map*) can read it via ctx.GetEndpoint().
Route template syntax
/orders/{id:int}
/users/{userId:guid}/posts/{postId:int}
/blog/{slug}
/files/{**filePath} # catch-all (multi-segment)
/products/{category=all} # default value
/items/{id?} # optional
/search/{*query} # catch-all single (legacy single-segment)
Route constraints
| Constraint | Matches |
|---|---|
int, bool, datetime, decimal, double, float, long, guid | Type |
min(N), max(N), range(min,max) | Numeric bounds |
length(N), length(min,max) | String length |
minlength(N), maxlength(N) | Min/max length |
alpha | a-zA-Z |
regex(...) | Pattern |
required | Non-empty |
app.MapGet("/orders/{id:int:min(1)}", (int id) => /* ... */);
app.MapGet("/users/{name:alpha:minlength(2)}", (string name) => /* ... */);
Custom constraints
public class EvenIntConstraint : IRouteConstraint
{
public bool Match(HttpContext? ctx, IRouter? r, string key, RouteValueDictionary values, RouteDirection dir)
=> values[key] is int n && n % 2 == 0;
}
builder.Services.Configure<RouteOptions>(o =>
o.ConstraintMap["even"] = typeof(EvenIntConstraint));
app.MapGet("/orders/{id:even}", (int id) => /* ... */);
Route groups
var v1 = app.MapGroup("/api/v1")
.WithTags("v1")
.RequireAuthorization();
var orders = v1.MapGroup("/orders");
orders.MapGet("/", GetAll);
orders.MapGet("/{id:int}", GetById);
orders.MapPost("/", Create);
Endpoint metadata
app.MapGet("/orders/{id:int}", GetOrder)
.WithName("GetOrder")
.WithSummary("Retrieve an order")
.WithDescription("Returns 404 if not found")
.WithTags("Orders")
.Produces<Order>(200)
.Produces(404)
.RequireAuthorization("OrdersRead");
WithName is essential for reverse URL generation.
Reverse routing
public class C(LinkGenerator lg)
{
public string MakeOrderUrl(int id)
=> lg.GetPathByName("GetOrder", new { id })!;
}
// In a controller:
public IActionResult Foo()
{
var url = Url.RouteUrl("GetOrder", new { id = 42 });
return Ok(url);
}
// In a Razor view:
// <a asp-route="GetOrder" asp-route-id="@Model.Id">View</a>
LinkGenerator is the modern, controller-free way (works in any service).
Match precedence
The framework uses route precedence based on:
- Number of segments (more = more specific).
- Constraint count.
- Static content (literal segment > parameter).
- Catch-all is least specific.
But within ambiguity, the framework throws AmbiguousMatchException rather than guessing. Disambiguate via Order or more specific templates.
Endpoint inspection
app.Use(async (ctx, next) =>
{
var endpoint = ctx.GetEndpoint();
if (endpoint?.Metadata.GetMetadata<RequireAuthorizationAttribute>() is not null)
Logger.LogInformation("Auth-required endpoint: {Name}", endpoint.DisplayName);
await next(ctx);
});
GetEndpoint() is non-null after UseRouting. Useful for endpoint-aware middleware (feature flags, custom auth, etc.).
Conventional vs attribute routing
// Conventional (MVC; kept for backward compat)
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Attribute (preferred)
[Route("api/[controller]")]
public class OrdersController { ... }
Attribute is more explicit; conventional is brief but harder to follow in large apps.
Slash and case sensitivity
builder.Services.Configure<RouteOptions>(o =>
{
o.LowercaseUrls = true;
o.AppendTrailingSlash = false;
});
LowercaseUrls = true standardizes generated URLs. Routing is case-insensitive by default for matching; this only affects URL generation.
MapFallback
app.MapGet("/", () => "Home");
app.MapFallback(() => Results.NotFound());
// Or, classic SPA serve-all:
app.MapFallbackToFile("index.html");
Catches anything no other route matched. Common pattern in SPA hosting.
Code: correct vs wrong
❌ Wrong: ambiguous routes
app.MapGet("/{id:int}", h1);
app.MapGet("/{name}", h2);
// AmbiguousMatchException for /5 (matches both)
✅ Correct: differentiate
❌ Wrong: catch-all before specific
app.MapGet("/{**path}", _ => /* fallback */);
app.MapGet("/about", () => "about"); // never reached for /about? actually still wins via specificity
The framework still picks the more specific. But it's a smell — write specifics first for readability.
❌ Wrong: hardcoded URLs
✅ Correct: reverse routing
Design patterns for this topic
Pattern 1 — "Attribute routing for APIs; group for organization"
- Intent: explicit, scannable URLs.
Pattern 2 — "Constraint everything"
- Intent: type/format-validated routes; clearer 404s for bad inputs.
Pattern 3 — "WithName + reverse routing"
- Intent: decouple URL generation from URL structure.
Pattern 4 — "MapGroup nesting"
- Intent: version + resource layered concerns.
Pattern 5 — "MapFallback for SPA hosting"
- Intent: server returns SPA shell for any unrecognized path.
Pros & cons / trade-offs
| Choice | Pros | Cons |
|---|---|---|
| Attribute routing | Explicit | Per-action |
| Conventional routing | DRY for similar apps | Hard to scan |
| Route constraints | Validation at routing | Verbose templates |
MapGroup | Reuse metadata | Easy to nest deeply |
LinkGenerator | Controller-free reverse routing | Slightly heavier than Url.* |
When to use / when to avoid
- Use attribute routing for APIs.
- Use route constraints for typed/format-validated tokens.
- Use
WithName+ reverse routing to avoid hard-coded URLs. - Avoid ambiguous templates — disambiguate proactively.
- Avoid
string.Format("/orders/{0}", id)— use reverse routing.
Interview Q&A
Q1. What does UseRouting do? Matches the request to an endpoint; sets HttpContext.Endpoint. Doesn't execute it.
Q2. What's a route constraint? A predicate on a route token: {id:int}, {slug:length(3,50)}. Failed match → 404.
Q3. Difference between {*query} and {**path}? Both catch-all. * matches a single greedy segment (URL-decoded). ** matches multi-segment (preserves slashes). Modern code uses **.
Q4. How do you generate URLs without hard-coding? Url.Action, Url.Page, Url.RouteUrl (in MVC), or LinkGenerator (anywhere via DI).
Q5. What's WithName for? Names the endpoint for reverse routing. Pair with LinkGenerator.GetPathByName to produce URLs.
Q6. How does match precedence work? More-specific routes win: more segments, more constraints, more literal content. Catch-all is least specific.
Q7. What's MapFallback? Catches any request no other route matched. Useful for SPAs (MapFallbackToFile("index.html")).
Q8. Custom constraint? Implement IRouteConstraint; register via RouteOptions.ConstraintMap.
Q9. Where can you read endpoint metadata? After UseRouting. ctx.GetEndpoint()?.Metadata.GetMetadata<TAttribute>().
Q10. Why might you choose conventional over attribute routing? Brief for many similar actions in MVC traditional sites. Most modern .NET projects prefer attribute or Minimal APIs.
Gotchas / common mistakes
- ⚠️ Ambiguous templates — runtime exception.
- ⚠️ Hard-coded URLs — break when routes change.
- ⚠️ Forgetting
WithName— can't reverse-route. - ⚠️
LowercaseUrlssilently changes generated URLs — coordinate with consumers. - ⚠️
UseEndpointsafter middleware that writes to response — might short-circuit before endpoint runs.