CodeOath
← All posts
.NET Core / Web API70 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Contents — Part 4 of 19: Routing: Attributes Map a URL to a Method
Part 4 of 19 · ~1 min

Routing: Attributes Map a URL to a Method

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Order>> GetById(int id)
    {
        var order = await _orderService.GetByIdAsync(id);
        if (order is null) return NotFound();
        return Ok(order);
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Order>>> GetAll([FromQuery] string? status)
    {
        var orders = await _orderService.GetAllAsync(status);
        return Ok(orders);
    }

    [HttpPost]
    public async Task<ActionResult<Order>> Create(CreateOrderRequest request)
    {
        var order = await _orderService.CreateAsync(request);
        return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
    }
}

[Route("api/[controller]")] maps this to api/orders (the [controller] token is replaced with the class name minus "Controller"). [HttpGet("{id}")] adds a route parameter, bound automatically to the id parameter by name; [HttpGet] with no template on GetAll matches the bare api/orders, and status arrives from a query string (?status=shipped) via [FromQuery].

Route templates support constraints, restricting what a segment is allowed to match before the framework even tries to invoke an action:

[HttpGet("{id:int}")]         // only matches if {id} parses as an int — "abc" 404s instead of erroring inside the method
[HttpGet("{id:int:min(1)}")]  // stacked constraints — int AND at least 1
[HttpGet("{slug:alpha}")]     // only matches alphabetic characters

When two routes could both match the same URL, ASP.NET Core resolves ambiguity using route precedence — a literal segment beats a constrained parameter, which beats an unconstrained parameter, so api/orders/pending and api/orders/{id:int} don't conflict even though both could theoretically match a URL like api/orders/pending.