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 6 of 19 · ~2 min

Model Binding: Where Parameters Come From

Strip away the automation and model binding is just this: taking pieces scattered across an incoming request and slotting each one into the matching parameter of whatever action method is about to run. [ApiController] does this invisibly for ReservationsController.Create; spelled out explicitly, the way the kiosk check-in endpoint needs to for one header nothing infers automatically, it looks like this:

[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(
    [FromRoute] int toolId,                    // from the URL path segment
    [FromRoute] Guid id,                        // from the URL path segment
    [FromQuery] bool includeCancelled,           // from ?includeCancelled=true
    [FromHeader(Name = "X-Kiosk-Id")] string? kioskId) // which physical kiosk is asking
{
    // ...
}

X-Kiosk-Id matters later — the lobby has three check-in stations, and knowing which one issued a request is half of how Dana eventually reconstructed the Tuesday-afternoon timeline.

Once you see it, [ApiController]'s inference logic boils down to shape, not to any keyword you typed: a plain scalar — int, Guid, string — that lines up with a slot in the route template gets pulled from the route itself. Anything bulkier, a class carrying several properties the way CreateReservationRequest does, with nowhere in the route for it to land, gets assumed to be sitting in the request body. Exactly one parameter, ever, gets to claim that body on a given action — inferred or spelled out with [FromBody] yourself, it doesn't matter which — and Bench found out the hard way that this ceiling isn't advisory. An early draft of the reservation endpoint tried to accept both the reservation details and a separate NotifyOptions object as two [FromBody] parameters on the same action, on the reasoning that it was "still just one POST." It never got as far as misbehaving under real traffic — MapControllers() refused to start Bench at all, failing immediately with an error that named both offending parameters and spelled out the one-body-parameter rule outright. That's the detail worth holding onto: this isn't a bug that surfaces under load weeks later, it's a wall the app hits at startup, every single time, before it ever serves a request. The actual fix was folding both into one CreateReservationRequest — which is the whole reason a request DTO exists in the first place.