Skip to main content

Routing


Attribute Routing in Controllers

You can use attribute routing to map controller action methods to HTTP methods and routes.

A typical RESTful controller with all standard HTTP verbs looks like this:

[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
// GET all: GET /api/products
[HttpGet]
public IActionResult GetAll() => Ok(repo.GetAll());

// GET by ID: GET /api/products/5
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
=> repo.Get(id) is { } prod ? Ok(prod) : NotFound();

// CREATE: POST /api/products
// Request body: JSON (sent as application/json)
// e.g. fetch('/api/products', {method:'POST',body:JSON.stringify(data),headers:{'Content-Type':'application/json'}})
[HttpPost]
public IActionResult Create([FromBody] ProductModel input)
{
var id = repo.Add(input);
return CreatedAtAction(nameof(GetById), new { id }, input);
}

// UPDATE: PUT /api/products/5
// Request body: JSON (full entity)
[HttpPut("{id:int}")]
public IActionResult Replace(int id, [FromBody] ProductModel input)
{
if (!repo.Exists(id)) return NotFound();
repo.Update(id, input);
return NoContent();
}

// PARTIAL UPDATE: PATCH /api/products/5
// Request body: JSON Patch (see RFC 6902)
[HttpPatch("{id:int}")]
public IActionResult PartialUpdate(int id, [FromBody] JsonPatchDocument<ProductModel> patchDoc)
{
var prod = repo.Get(id);
if (prod == null) return NotFound();
patchDoc.ApplyTo(prod, ModelState);
if (!ModelState.IsValid) return BadRequest(ModelState);
repo.Update(id, prod);
return NoContent();
}

// DELETE: DELETE /api/products/5
[HttpDelete("{id:int}")]
public IActionResult Delete(int id)
{
if (!repo.Exists(id)) return NotFound();
repo.Delete(id);
return NoContent();
}

// HEAD: HEAD /api/products/5
[HttpHead("{id:int}")]
public IActionResult Head(int id)
=> repo.Exists(id) ? Ok() : NotFound();

// SEARCH: GET /api/products/search/laptop
// Query param alternative: GET /api/products?term=laptop
[HttpGet("search/{term}")]
public IActionResult Search(string term)
=> Ok(repo.FindByName(term));
}

Routing in a Nutshell

  • [Route("api/products")] is the base URL for all actions.
  • Attributes like [HttpGet("{id:int}")] map HTTP verbs and define route parameters (here, an integer id from the path).
  • [FromBody] indicates data is expected in the request body (e.g., JSON).
  • Query parameters: For /api/products?sort=name, add a method parameter string sort.
  • Two actions can’t have the same route and verb (ambiguous).

Custom/Composite Routing Examples

You can mix-and-match route templates and HTTP methods for any scenario:

// Action bound to GET with custom route segment:
[HttpGet("details/{id:int}/{extra?}")]
public IActionResult Details(int id, string? extra) { ... }

// POST to a subresource
[HttpPost("{id}/comments")]
public IActionResult AddComment(int id, CommentDto dto) { ... }

// PUT by name (non-RESTful/cusom pattern)
[HttpPut("by-name/{name}")]
public IActionResult UpdateByName(string name, ProductModel input) { ... }

// Multiple verbs on one action:
[HttpPost, HttpPatch("partial")]
public IActionResult Upsert(ProductModel input) { ... }

Classic Routing Pipeline (For Context)

In Program.cs:

app.UseRouting();
app.UseEndpoints((endpoints) => {
endpoints.MapControllers(); // Enables attribute-routed controllers
});

For Minimal APIs (Not in controller), you’d use:

app.MapGet("/items", ...);
app.MapPost("/items", ...);
// etc.