Binding request data

ASP.NET Core · Essentials · 12 min

Correct binding gives you typed, validated input instead of parsing HttpContext by hand.

Minimal API handlers declare what they need as parameters, and ASP.NET Core binds each one from the request. The rules, roughly in order:

  • Explicit attributes win: [FromRoute], [FromQuery], [FromHeader(Name = "X-Api-Key")], [FromBody], [FromForm], [FromServices].
  • Special types are injected directly: HttpContext, HttpRequest, HttpResponse, CancellationToken (canceled when the client disconnects), ClaimsPrincipal.
  • A parameter whose name matches a route parameter binds from the route.
  • Simple types (int, string, Guid, DateTime, enums, anything with TryParse) otherwise bind from the query string. Arrays like int[] ids bind from repeated keys ?ids=1&ids=2.
  • Types registered in DI bind from services.
  • Any other complex type binds from the JSON body using System.Text.Json (camelCase by default). Only one body parameter is allowed.

Nullability matters: string? q or a default value (int page = 1) makes a query parameter optional; a non-nullable one that is missing produces a 400.

[AsParameters] groups many parameters into one record or class, keeping handler signatures tidy.

Records are a natural fit for request bodies: record CreateTodo(string Title, DateOnly? Due);. Don't bind directly to database entities; use separate request models so clients can't set fields like Id or IsAdmin.

Example

using Microsoft.AspNetCore.Mvc;

var app = WebApplication.Create(args);

app.MapGet("/search", (string q, int page = 1, int[]? tags = null) =>
    $"q={q} page={page} tags={string.Join(",", tags ?? [])}");

app.MapGet("/whoami", ([FromHeader(Name = "X-User")] string? user) =>
    user ?? "anonymous");

app.MapPost("/todos", (CreateTodo body) =>
    Results.Ok(new { body.Title, Due = body.Due?.ToString("yyyy-MM-dd") }));

app.MapGet("/orders/{id:int}", ([AsParameters] OrderQuery query) =>
    $"order {query.Id}, details={query.Details}");

app.Run();
// GET /search?q=cs&tags=1&tags=2 -> q=cs page=1 tags=1,2
// GET /search                    -> 400 (q is required)
// POST /todos {"title":"Ship","due":"2026-10-20"} -> {"title":"Ship","due":"2026-10-20"}
// GET /orders/5?details=true     -> order 5, details=True

record CreateTodo(string Title, DateOnly? Due);
record OrderQuery(int Id, bool Details = false);

Remember

  • Route names bind from the route, simple types from the query, complex types from the JSON body
  • Nullable or defaulted parameters are optional; missing required ones give 400
  • Use [FromHeader], [FromQuery] etc. to be explicit
  • Bind to request records, not database entities

Watch out for

  • Declaring two complex-type parameters and expecting both to come from the body.

Check yourself

  1. For app.MapGet("/items", (int page) => ...), what does GET /items (no query string) return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding