.NET Core / Web API71 min total · 19 parts
Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection
Part 8 of 19 · ~2 min
Action Results and Status Codes
[HttpPost]
public async Task<ActionResult<Reservation>> Create(int toolId, CreateReservationRequest request)
{
var result = await _reservations.CreateAsync(toolId, request);
return result switch
{
{ Conflict: true } => Conflict(new { message = "That slot is no longer available." }),
_ => CreatedAtAction(nameof(GetById), new { toolId, id = result.Reservation!.Id }, result.Reservation)
};
}
That switch only compiles because of what ActionResult<T> buys you: hand back a bare Reservation and it's read automatically as a 200 carrying that object, or hand back any IActionResult — Conflict, CreatedAtAction, whatever a given branch actually calls for — and neither path forces the method to commit to one return type up front.
| Helper | Status | Bench's use of it |
|---|---|---|
Ok(value) | 200 | A tool lookup, or a successful list of a member's reservations |
CreatedAtAction(...) | 201 | A confirmed reservation — Location points straight at it |
NoContent() | 204 | A cancelled reservation — nothing meaningful to hand back |
BadRequest(...) | 400 | DurationMinutes out of range, StartUtc missing |
Unauthorized() | 401 | No CampusKey token presented at all |
Forbid() | 403 | Signed in, but not staff, hitting a staff-only override |
NotFound() | 404 | A reservation ID that doesn't exist for this tool |
Conflict(...) | 409 | The slot this request wants is already booked |
That Conflict(...) row is the one to hold onto — it's what Bench is supposed to return the moment two people try to claim the same slot, and it's exactly the response that never fired for Marcus and Yuki. Keep that in mind; it comes back twice more, once the actual mechanism is on the table.
Look closely at what that CreatedAtAction call is actually doing, because it's easy to skim past. Instead of typing out a URL, it hands the framework an action name and a bag of route values and lets the framework work backward to whatever address that action would actually answer at, filling in the Location header with the result. Change GetById's route template six months from now and this line needs zero edits — there was never a string in it to go stale in the first place, unlike a URL somebody hand-typed once and forgot about.