The foundation: what the official documentation explains
Minimal API parameter binding can obtain values from routes, query strings, headers, request bodies, and registered services. Special types such as HttpContext and CancellationToken are supplied by the framework. Binding establishes a value or reports a conversion problem; it does not prove that the value satisfies the application business rules.
Implementation context: The examples use a .NET 10 MVC application organized into feature folders (Vertical Slice Architecture). Basic C# classes and async/await are assumed. Reference excerpts show selected parts of that application; separately labeled teaching adaptations explain alternatives. They are not complete standalone projects.
Read the signature as an input map
In the Country create route, CreateCountryRequest represents the JSON body. AppDbContext is a registered service, and CancellationToken represents request cancellation. The database context is not something the browser serializes and sends. Understanding that split makes a long lambda signature much less mysterious.
The host application is MVC, but this particular route is a Minimal API endpoint. MVC model-binding conventions and controller attributes should not be assumed to govern every Minimal API parameter. Always identify which endpoint style you are reading before applying a tutorial to it.
Reference excerpt: Areas/Admin/Country/Endpoints/CountryEndpoint.cs
// POST /api/country
group.MapPost("/", async (CreateCountryRequest request, AppDbContext db, CancellationToken ct) =>
{
var handler = new CreateCountryHandler(db);
var result = await handler.HandleAsync(request, ct);
return result.Success
? Results.Created($"/api/country/{result.Data?.Id}", result)
: Results.BadRequest(result);
})
.WithName("CreateCountry");
Keep the request smaller than the entity
Country creation accepts code, name, and description. It does not accept audit timestamps or a deletion flag. The handler explicitly maps the allowed fields to a new entity. This makes the write contract visible: a caller cannot update every mapped database property merely because the entity contains it.
The response returns an identifier and code because the Create screen needs them for confirmation and navigation. The response shape is a separate decision from the write contract. Both can change for different reasons, which is why using one large entity class for request, persistence, and response is often confusing.
Reference excerpt: Areas/Admin/Country/Cqrs/CreateCountryHandler.cs
public class CreateCountryRequest
{
public string? Code { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
}
public class CreateCountryResponse
{
public string? Id { get; set; }
public string? Code { get; set; }
}
Match the method, content type, and body
A representative payload is shown below. The browser sends it with Content-Type: application/json. The DTO uses PascalCase C# properties; the example frontend uses camelCase JSON fields. The application relies on its HTTP JSON behavior to bridge that representation. Do not send form-encoded data to this body contract and expect the JSON DTO path to behave identically.
Example POST /api/country JSON body
{
"code": "ID",
"name": "Indonesia",
"description": "Learning example"
}
Use a disposable code not already present when trying a create operation. The handler performs a duplicate-code check, so an existing code can produce a business failure even though the JSON bound correctly. Removing the name also produces a business validation failure. Breaking the JSON syntax is different: the handler may not be reached at all.
Compare route input and body input
Country detail uses GET /api/country/{id}, so the identifier comes from the route. Country update uses PUT /api/country and includes the identifier in UpdateCountryRequest. That is the actual contract here; do not invent a PUT route with an identifier suffix just because another REST example uses one.
An application could choose PUT /api/country/{id} instead, but it would need matching endpoint and browser changes and a rule for reconciling route and body identifiers. This guide documents the existing implementation shape so a beginner can trace it accurately.
Reference excerpt: Areas/Admin/Country/Cqrs/UpdateCountryHandler.cs
public class UpdateCountryRequest
{
public string? Id { get; set; }
public string? Code { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
}
Interpret the response envelope
The project wraps outcomes in ApiResponse<T> with Success, Message, Data, and Errors. A false Success value is an application outcome, while the endpoint also selects an HTTP status. Check both in the client. A proxy or error middleware can return a different body entirely; calling response.json unconditionally does not guarantee that the server returned JSON.
Keep the cancellation token flowing into the handler and EF operations. It allows cooperative cancellation; it is not a rollback guarantee for a write that has already committed. Browser cancellation and database transaction semantics solve different problems.
Practice with three different failures
In a local API client, compare malformed JSON, a valid JSON object with a missing name, and a duplicate country code. Record whether the handler ran, the endpoint result, and the final response seen by the client. These three cases exercise parsing, validation, and business rules respectively. If they all look identical in the UI, improve the diagnosis before changing the DTO.
Key Takeaways
- A parameter can come from HTTP input, DI, or a framework-provided value.
- Request DTOs define the writable surface of an operation.
- Binding success, business success, and HTTP status are separate signals.
FAQ
Is AppDbContext read from JSON?
No. It is resolved as a registered service in this endpoint.
Does a nullable Name mean an empty name is allowed?
No. The DTO can represent missing input; the validator determines whether it is acceptable.
Why does update use PUT without an id in the route?
This implementation carries Id in its update request body. Follow the declared endpoint contract.