Skip to content

.NET Core · ASP.NET Core

Routing in ASP.NET Core

6 min read Updated 2026-06-23 Share:

Practice Routing interview questions

Why routing knowledge is tested in .NET interviews

Routing is how every HTTP request finds its handler. Interviewers probe it because wrong route design causes security holes (wrong controller handles a sensitive route), incorrect behavior (ambiguous routes match the wrong action), and broken clients (URLs change and break existing integrations). Understanding routing deeply signals architectural maturity.

How endpoint routing works

Endpoint routing (ASP.NET Core 3.0+) separates route matching from route dispatching. UseRouting matches the request to an endpoint and stores the result in HttpContext. Middleware running between UseRouting and MapControllers can read that endpoint metadata:

app.UseRouting();            // 1. Match: sets HttpContext.GetEndpoint()
app.UseAuthentication();     // 2. Can now inspect [Authorize] on the matched endpoint
app.UseAuthorization();      // 3. Enforce auth before dispatch
app.MapControllers();        // 4. Dispatch: actually run the matched action

This is why UseAuthorization must come after UseRouting — authorization middleware needs to know which endpoint matched before it can check [Authorize] attributes.

Conventional routing vs attribute routing

Conventional routing defines URL patterns centrally. Every controller that doesn't have its own [Route] is matched by these patterns:

// MVC — one pattern for all controllers:
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
// /Products/Details/5 → ProductsController.Details(id: 5)
// /           → HomeController.Index()
// /Blog/Post/42 → BlogController.Post(id: 42)

Attribute routing puts templates directly on controllers and actions:

[ApiController]
[Route("api/[controller]")]          // [controller] expands to "products"
public class ProductsController : ControllerBase
{
    [HttpGet]                        // GET /api/products
    public IActionResult List() => Ok(_products);

    [HttpGet("{id:int}")]            // GET /api/products/5
    public IActionResult Get(int id) => Ok(Find(id));

    [HttpPost]                       // POST /api/products
    public IActionResult Create([FromBody] CreateProductDto dto) { ... }

    [HttpPut("{id:int}")]            // PUT /api/products/5
    public IActionResult Update(int id, [FromBody] UpdateProductDto dto) { ... }

    [HttpDelete("{id:int}")]         // DELETE /api/products/5
    public IActionResult Delete(int id) { ... }
}

When to use which:

  • Attribute routing: Web APIs, REST endpoints, fine-grained URL control.
  • Conventional routing: MVC apps with consistent controller/action/id URLs.
  • [ApiController] requires attribute routing — it disables conventional routing for the controller.

Route templates in depth

// Literal segment — exact match required:
[HttpGet("api/v1/status")]          // only /api/v1/status

// Route parameter — captures one path segment:
[HttpGet("products/{id}")]          // /products/42 → id = "42"

// Optional parameter — segment may be absent:
[HttpGet("search/{term?}")]         // /search and /search/shoes both match
public IActionResult Search(string? term) { ... }

// Default value:
[HttpGet("page/{number=1}")]        // /page → number=1; /page/3 → number=3

// Multiple parameters:
[HttpGet("{year:int}/{month:int}/{slug}")]
// /2026/6/my-post → year=2026, month=6, slug="my-post"

// Catch-all parameter — captures the rest including slashes:
[HttpGet("files/{**path}")]         // /files/a/b/c.txt → path = "a/b/c.txt"

Token replacement:

  • [controller] → controller class name minus "Controller" suffix
  • [action] → action method name
  • [area] → area name

Route constraints — right tool for the right job

Constraints restrict which requests a route matches. They are specified with a colon:

[HttpGet("{id:int}")]             // id must be parseable as int
[HttpGet("{id:int:min(1)}")]      // int ≥ 1
[HttpGet("{id:guid}")]            // GUID format
[HttpGet("{slug:alpha}")]         // letters only
[HttpGet("{date:datetime}")]      // parseable DateTime
[HttpGet("{code:length(5)}")]     // exactly 5 characters
[HttpGet("{zip:regex(^\\d{{5}}$)}")] // 5-digit zip code

// These two routes are unambiguous because of constraints:
[HttpGet("{id:int}")]             // matches /products/5
[HttpGet("{name:alpha}")]         // matches /products/widget

Custom route constraint:

public class EvenConstraint : IRouteConstraint
{
    public bool Match(HttpContext? httpContext, IRouter? route,
        string routeKey, RouteValueDictionary values, RouteDirection direction)
    {
        return values.TryGetValue(routeKey, out var val)
            && int.TryParse(val?.ToString(), out int n)
            && n % 2 == 0;
    }
}

builder.Services.Configure<RouteOptions>(o =>
    o.ConstraintMap["even"] = typeof(EvenConstraint));

app.MapGet("/items/{id:even}", (int id) => $"Even: {id}");

Important: route constraints are for routing, not validation. A constraint of {id:int:min(1)} rejects non-integer URLs at the routing layer, but you still need business validation in the action for input correctness. A URL that doesn't match any route returns 404, not 400 — the client may be confused if you rely on constraints for validation.

Always generate URLs programmatically — hard-coded strings break when you rename a route:

// In a controller — Url helper:
[HttpPost]
public IActionResult Create([FromBody] CreateOrderDto dto)
{
    var order = _svc.Create(dto);
    var url = Url.Action("Get", "Orders", new { id = order.Id });
    return Created(url, order); // Location: /api/orders/42
}

// Named route:
[HttpGet("{id:int}", Name = "GetOrder")]
public IActionResult Get(int id) => Ok(Find(id));

var url = Url.RouteUrl("GetOrder", new { id = 5 }); // /api/orders/5

// LinkGenerator in DI (works outside controllers):
public class NotificationService
{
    private readonly LinkGenerator _links;
    public NotificationService(LinkGenerator links) => _links = links;

    public string GetOrderUrl(int orderId)
        => _links.GetPathByName("GetOrder", new { id = orderId })!;
}

For minimal APIs, use .WithName() to enable link generation:

app.MapGet("/orders/{id:int}", (int id) => GetOrder(id))
   .WithName("get-order");

app.MapPost("/orders", (CreateOrderDto dto, LinkGenerator links) =>
{
    var order = CreateOrder(dto);
    var url = links.GetPathByName("get-order", new { id = order.Id });
    return Results.Created(url, order);
});

Minimal APIs and route groups

Minimal APIs define endpoints with lambdas — no controller class needed:

// Route group: common prefix + shared policies
var orders = app.MapGroup("/api/orders")
                .RequireAuthorization()
                .WithTags("Orders");

orders.MapGet("/", async (AppDb db) =>
    await db.Orders.ToListAsync());

orders.MapGet("/{id:int}", async (int id, AppDb db) =>
    await db.Orders.FindAsync(id) is Order o ? Results.Ok(o) : Results.NotFound());

orders.MapPost("/", async (CreateOrderDto dto, AppDb db) =>
{
    var order = new Order { Item = dto.Item };
    db.Orders.Add(order);
    await db.SaveChangesAsync();
    return Results.Created($"/api/orders/{order.Id}", order);
});

orders.MapDelete("/{id:int}", async (int id, AppDb db) =>
{
    if (await db.Orders.FindAsync(id) is Order o)
    {
        db.Orders.Remove(o);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }
    return Results.NotFound();
});

Route groups eliminate repetition — the /api/orders prefix, RequireAuthorization(), and the WithTags("Orders") Swagger tag apply to every endpoint in the group.

Routing ambiguity — and how to avoid it

When two routes match with equal specificity, AmbiguousMatchException is thrown at runtime:

// Ambiguous — both match GET /products/5:
[HttpGet("{id}")]
public IActionResult GetById(string id) { ... }

[HttpGet("{name}")]
public IActionResult GetByName(string name) { ... }

// Fixed with constraints:
[HttpGet("{id:int}")]      // only integers
[HttpGet("{name:alpha}")]  // only letters

// Or with distinct templates:
[HttpGet("by-id/{id:int}")]
[HttpGet("by-name/{name}")]

For conventional routing, more specific routes must be registered first:

app.MapControllerRoute("featured", "products/featured", ...); // specific first
app.MapControllerRoute("default",  "products/{id}",     ...); // generic second

HTTP verb attributes and REST conventions

The HTTP verb attributes constrain routes to specific methods and follow REST semantics:

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet]                     // GET /api/orders — list collection
    [HttpGet("{id:int}")]         // GET /api/orders/5 — get single
    [HttpPost]                    // POST /api/orders — create
    [HttpPut("{id:int}")]         // PUT /api/orders/5 — full replace
    [HttpPatch("{id:int}")]       // PATCH /api/orders/5 — partial update
    [HttpDelete("{id:int}")]      // DELETE /api/orders/5 — delete
    [HttpHead("{id:int}")]        // HEAD /api/orders/5 — existence check
}

[AcceptVerbs] for multi-verb actions:

[AcceptVerbs("GET", "HEAD")]
public IActionResult GetOrHead(int id) { ... }

Never use GET for operations with side effects — caches and proxies assume GET is safe and idempotent, which breaks if GET mutates state.

Recap

Endpoint routing separates matching (UseRouting) from dispatch (MapControllers), letting middleware between them inspect endpoint metadata. Attribute routing gives per-action URL control and is required with [ApiController]. Route constraints restrict matching — use them for routing, not input validation. Always generate URLs with IUrlHelper or LinkGenerator, never hard-code strings. Use MapGroup in minimal APIs to share prefixes and policies across related endpoints. Resolve ambiguity with constraints or distinct templates — never rely on registration order for correctness.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel