Skip to main content
CodeOath
← All posts

.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.

HelperStatusBench's use of it
Ok(value)200A tool lookup, or a successful list of a member's reservations
CreatedAtAction(...)201A confirmed reservation — Location points straight at it
NoContent()204A cancelled reservation — nothing meaningful to hand back
BadRequest(...)400DurationMinutes out of range, StartUtc missing
Unauthorized()401No CampusKey token presented at all
Forbid()403Signed in, but not staff, hitting a staff-only override
NotFound()404A reservation ID that doesn't exist for this tool
Conflict(...)409The 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.