Skip to content

Routing

Key Points

  • Endpoint routing is the modern model: UseRouting() matches the URL to an endpoint; UseEndpoints() (or implicit Map* 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.Page produces URLs from route names + values.
  • Route groups (MapGroup) inherit metadata; nesting allowed.

Concepts (deep dive)

Endpoint routing pipeline

UseRouting()         UseEndpoints() / app.MapXxx
    │                       │
    ▼                       ▼
   match endpoint     execute matched endpoint

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:

  1. Number of segments (more = more specific).
  2. Constraint count.
  3. Static content (literal segment > parameter).
  4. Catch-all is least specific.
app.MapGet("/{slug}", _ => "catch all");
app.MapGet("/about", () => "about");        // wins for /about

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

app.MapGet("/by-id/{id:int}", h1);
app.MapGet("/by-name/{name}", h2);

❌ 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

return Redirect("/orders/" + id);

✅ Correct: reverse routing

return RedirectToRoute("GetOrder", new { id });

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.
  • ⚠️ LowercaseUrls silently changes generated URLs — coordinate with consumers.
  • ⚠️ UseEndpoints after middleware that writes to response — might short-circuit before endpoint runs.

Further reading