Overview
Parameter binding in ASP.NET Core is the process of taking data from an HTTP request and converting it into strongly typed C# parameters that an API endpoint can use. Instead of manually reading HttpContext.Request.RouteValues, Request.Query, Request.Headers, Request.Body, or Request.Form, ASP.NET Core can bind those values directly to action method parameters, Minimal API handler parameters, DTOs, and services.
This topic matters because almost every Web API endpoint depends on parameter binding. A typical API may receive an id from the route, filtering options from the query string, a JSON object from the request body, a file from form data, a tenant or correlation value from headers, and business services from dependency injection. Understanding how these sources are selected helps developers design predictable APIs, avoid security issues such as overposting, troubleshoot 400 Bad Request errors, and write cleaner endpoint code.
For interviews, parameter binding is important because it reveals whether a developer understands the HTTP request pipeline, controller APIs, Minimal APIs, DTO design, model validation, dependency injection, and common production mistakes. Strong candidates can explain not only how to use attributes like [FromRoute], [FromQuery], [FromBody], [FromForm], [FromHeader], and [FromServices], but also when explicit binding is safer than relying on framework inference.
Core Concepts
What Parameter Binding Means
Parameter binding maps incoming request data to .NET values.
For example, consider this HTTP request:
POST /api/products/42?includeReviews=true
X-Correlation-Id: abc-123
Content-Type: application/json
{
"name": "Mechanical Keyboard",
"price": 120
}
An ASP.NET Core endpoint can bind the request into C# parameters:
[HttpPost("api/products/{id:int}")]
public IActionResult UpdateProduct(
[FromRoute] int id,
[FromQuery] bool includeReviews,
[FromHeader(Name = "X-Correlation-Id")] string correlationId,
[FromBody] UpdateProductRequest request)
{
return Ok(new
{
id,
includeReviews,
correlationId,
request.Name,
request.Price
});
}
public sealed class UpdateProductRequest
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
The endpoint receives strongly typed values without manually parsing strings or reading the body.
Common Binding Sources
ASP.NET Core commonly binds parameters from these request sources:
Explicit attributes are useful because they make the API contract clear. They also reduce confusion when a parameter could theoretically come from multiple places.
Controllers vs Minimal APIs
Both controller-based APIs and Minimal APIs support parameter binding, but their style differs.
Controller example:
[ApiController]
[Route("api/products")]
public sealed class ProductsController : ControllerBase
{
[HttpGet("{id:int}")]
public IActionResult GetProduct(
[FromRoute] int id,
[FromQuery] bool includeReviews,
[FromServices] IProductService productService)
{
var product = productService.GetById(id, includeReviews);
return product is null ? NotFound() : Ok(product);
}
}
Minimal API example:
app.MapGet("/api/products/{id:int}", (
[FromRoute] int id,
[FromQuery] bool includeReviews,
IProductService productService) =>
{
var product = productService.GetById(id, includeReviews);
return product is null ? Results.NotFound() : Results.Ok(product);
});
In Minimal APIs, services registered in dependency injection are commonly inferred automatically by type, so [FromServices] is often optional. In controllers, constructor injection is usually preferred for services that are used across multiple actions, while [FromServices] is useful for action-specific dependencies.
Binding from Route Values
Route values come from placeholders in the route template.
[HttpGet("api/orders/{orderId:int}/items/{itemId:int}")]
public IActionResult GetOrderItem(
[FromRoute] int orderId,
[FromRoute] int itemId)
{
return Ok(new { orderId, itemId });
}
Route binding is best for identifying a resource or nested resource.
Good route parameter examples:
GET /api/products/10
GET /api/orders/100/items/5
GET /api/customers/25/invoices
Common mistakes include using route parameters for optional filters or placing too much search state in the path.
Less ideal:
GET /api/products/category/electronics/min-price/100/max-price/500/sort/name
Better:
GET /api/products?category=electronics&minPrice=100&maxPrice=500&sort=name
Route values should be stable, meaningful parts of the resource identity. Optional filters usually belong in the query string.
Binding from Query String
Query string binding is used for optional values, filters, pagination, sorting, search, and flags.
[HttpGet("api/products")]
public IActionResult SearchProducts(
[FromQuery] string? search,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? sort = null)
{
return Ok(new { search, page, pageSize, sort });
}
Example request:
GET /api/products?search=keyboard&page=2&pageSize=10&sort=price_desc
For many query parameters, a request object is cleaner:
public sealed class ProductSearchQuery
{
public string? Search { get; init; }
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 20;
public string? Sort { get; init; }
}
[HttpGet("api/products")]
public IActionResult SearchProducts([FromQuery] ProductSearchQuery query)
{
return Ok(query);
}
Query string values are strings in HTTP. Model binding converts them to C# types such as int, bool, DateTime, Guid, enums, arrays, and collections when possible.
[HttpGet("api/products/by-tags")]
public IActionResult GetByTags([FromQuery] string[] tags)
{
return Ok(tags);
}
Possible request:
GET /api/products/by-tags?tags=keyboard&tags=wireless
Binding from the Body
Body binding is commonly used for JSON payloads in create and update operations.
public sealed class CreateProductRequest
{
public string Name { get; init; } = string.Empty;
public decimal Price { get; init; }
public int CategoryId { get; init; }
}
[HttpPost("api/products")]
public IActionResult CreateProduct([FromBody] CreateProductRequest request)
{
return Created($"/api/products/123", request);
}
Example request:
POST /api/products
Content-Type: application/json
{
"name": "Wireless Mouse",
"price": 35,
"categoryId": 7
}
In controller APIs with [ApiController], complex types are often inferred from the body. However, using [FromBody] can still make the API contract more explicit.
Minimal API example:
app.MapPost("/api/products", (CreateProductRequest request) =>
{
return Results.Created("/api/products/123", request);
});
For POST, PUT, and PATCH, Minimal APIs commonly infer complex parameters from the body. For GET, HEAD, OPTIONS, and DELETE, body binding is not implicitly inferred in the same way and should be explicit if it is truly needed.
A practical rule is: use body binding for complex create/update command data, not for resource identity or simple filters.
Only One Body Should Usually Be Bound
The request body is a stream. In typical API design, an endpoint should bind one main request body object.
Avoid this:
[HttpPost("api/orders")]
public IActionResult CreateOrder(
[FromBody] CustomerDto customer,
[FromBody] OrderDto order)
{
return Ok();
}
Prefer one request DTO:
public sealed class CreateOrderRequest
{
public CustomerDto Customer { get; init; } = new();
public OrderDto Order { get; init; } = new();
}
[HttpPost("api/orders")]
public IActionResult CreateOrder([FromBody] CreateOrderRequest request)
{
return Ok();
}
One body DTO gives the endpoint a clear contract and avoids ambiguity.
Binding from Form Data
Form binding reads values from posted form fields. It is commonly used for traditional HTML forms and multipart/form-data requests.
Controller example:
public sealed class UploadAvatarRequest
{
public string DisplayName { get; set; } = string.Empty;
public IFormFile Avatar { get; set; } = default!;
}
[HttpPost("api/users/avatar")]
public async Task<IActionResult> UploadAvatar([FromForm] UploadAvatarRequest request)
{
if (request.Avatar.Length == 0)
{
return BadRequest("File is empty.");
}
await using var stream = System.IO.File.Create($"uploads/{request.Avatar.FileName}");
await request.Avatar.CopyToAsync(stream);
return Ok(new { request.DisplayName, request.Avatar.FileName });
}
Minimal API example:
app.MapPost("/api/users/avatar", async (
[FromForm] string displayName,
IFormFile avatar) =>
{
if (avatar.Length == 0)
{
return Results.BadRequest("File is empty.");
}
await using var stream = File.Create($"uploads/{avatar.FileName}");
await avatar.CopyToAsync(stream);
return Results.Ok(new { displayName, avatar.FileName });
});
Form binding is different from JSON body binding. A request cannot be both normal JSON and multipart/form-data at the same time in the same payload format. File upload endpoints usually use form data, while normal API create/update endpoints usually use JSON.
For production file uploads, do not blindly trust IFormFile.FileName. Validate file size, extension, content type, storage location, authorization, and malware scanning requirements.
Binding from Headers
Header binding reads values from HTTP headers.
[HttpGet("api/orders/{id:int}")]
public IActionResult GetOrder(
[FromRoute] int id,
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId)
{
return Ok(new { id, correlationId, tenantId });
}
Headers are useful for request metadata, not normal business payload.
Common examples include:
Avoid using custom headers for data that belongs in the route, query string, or body. Also avoid trusting sensitive header values unless they come from a trusted gateway, authentication system, or validated middleware.
Binding from Services
Service binding gets values from the dependency injection container.
Controller action-level service binding:
[HttpGet("api/products/{id:int}")]
public async Task<IActionResult> GetProduct(
[FromRoute] int id,
[FromServices] IProductRepository repository)
{
var product = await repository.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}
Controller constructor injection is usually preferred when the service is used by multiple actions:
[ApiController]
[Route("api/products")]
public sealed class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetProduct([FromRoute] int id)
{
var product = await _repository.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}
}
Minimal API service binding:
app.MapGet("/api/products/{id:int}", async (
int id,
IProductRepository repository) =>
{
var product = await repository.GetByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});
In Minimal APIs, if a parameter type is registered in dependency injection, the framework can infer that it should come from services. [FromServices] can still be used for clarity.
Binding Inference and Why Explicit Binding Helps
ASP.NET Core can infer binding sources in many cases, especially with [ApiController] and Minimal APIs.
Controller example with inference:
[ApiController]
[Route("api/products")]
public sealed class ProductsController : ControllerBase
{
[HttpPost("{id:int}")]
public IActionResult Update(int id, UpdateProductRequest request)
{
return Ok(new { id, request.Name });
}
}
The framework can infer that id comes from route and request comes from body.
However, explicit binding is often better for interview-quality and production-quality code:
[HttpPost("{id:int}")]
public IActionResult Update(
[FromRoute] int id,
[FromBody] UpdateProductRequest request)
{
return Ok(new { id, request.Name });
}
Explicit binding makes it easier for readers, reviewers, and API consumers to understand where each value comes from.
Minimal API Binding Precedence
Minimal APIs use a set of rules to decide where a parameter comes from. A simplified practical version is:
- Use explicit attributes first, such as
[FromRoute],[FromQuery],[FromHeader],[FromBody],[FromForm],[FromServices], or[AsParameters]. - Bind special framework types directly, such as
HttpContext,HttpRequest,HttpResponse,ClaimsPrincipal,CancellationToken,IFormFile,Stream, orPipeReader. - Use custom binding methods like
BindAsyncwhen available. - Use
TryParsefor simple parseable values from route or query. - Use dependency injection if the parameter type is registered as a service.
- Use the request body for remaining complex parameters when body binding is allowed.
This matters because ambiguous Minimal API parameters may not come from the source a developer expects. For example, a type registered in dependency injection may be resolved as a service instead of being bound from the body.
Simple Types vs Complex Types
Simple types are values that can usually be converted from a string.
Examples:
int id
bool includeInactive
Guid customerId
DateTime startDate
decimal minPrice
ProductStatus status
These are often bound from route or query string.
Complex types are objects with multiple properties.
public sealed class CreateCustomerRequest
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
Complex types are commonly bound from the body for JSON requests or from the query string/form when explicitly specified.
A common interview mistake is saying, “Simple types always come from query and complex types always come from body.” That is too simplistic. Route templates, explicit attributes, HTTP method, [ApiController], Minimal API rules, custom binders, and registered services can affect the actual binding source.
Custom Binding with TryParse
Custom value types can participate in binding by exposing a TryParse method.
public readonly record struct ProductCode(string Value)
{
public static bool TryParse(string? value, out ProductCode productCode)
{
if (!string.IsNullOrWhiteSpace(value) && value.StartsWith("PRD-"))
{
productCode = new ProductCode(value);
return true;
}
productCode = default;
return false;
}
}
app.MapGet("/api/products/{code}", (ProductCode code) =>
{
return Results.Ok(new { code.Value });
});
This keeps parsing logic close to the value object and avoids repeating parsing code inside endpoints.
Custom Binding with BindAsync
Minimal APIs can use BindAsync for more advanced binding scenarios.
public sealed class PagingOptions
{
public int Page { get; init; }
public int PageSize { get; init; }
public static ValueTask<PagingOptions?> BindAsync(HttpContext context)
{
int.TryParse(context.Request.Query["page"], out var page);
int.TryParse(context.Request.Query["pageSize"], out var pageSize);
return ValueTask.FromResult<PagingOptions?>(new PagingOptions
{
Page = page <= 0 ? 1 : page,
PageSize = pageSize <= 0 ? 20 : Math.Min(pageSize, 100)
});
}
}
app.MapGet("/api/products", (PagingOptions paging) =>
{
return Results.Ok(paging);
});
BindAsync is useful when binding requires multiple request values or custom normalization. It should remain simple and predictable. Complex business logic should stay in application services, not in binders.
Grouping Parameters with AsParameters in Minimal APIs
Minimal APIs can group multiple parameters into one object using [AsParameters].
public sealed class SearchProductsRequest
{
[FromQuery]
public string? Search { get; init; }
[FromQuery]
public int Page { get; init; } = 1;
[FromQuery]
public int PageSize { get; init; } = 20;
[FromHeader(Name = "X-Correlation-Id")]
public string? CorrelationId { get; init; }
}
app.MapGet("/api/products", ([AsParameters] SearchProductsRequest request) =>
{
return Results.Ok(request);
});
This is helpful when a Minimal API handler has too many parameters. It can improve readability without forcing all values into the body.
Model Validation and Binding
Binding answers the question: “Can the request data be converted into .NET parameters?”
Validation answers the question: “Are the converted values acceptable for the application?”
Example DTO:
public sealed class CreateProductRequest
{
[Required]
[StringLength(100)]
public string Name { get; init; } = string.Empty;
[Range(0.01, 1_000_000)]
public decimal Price { get; init; }
}
Controller with [ApiController]:
[ApiController]
[Route("api/products")]
public sealed class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create([FromBody] CreateProductRequest request)
{
return Ok(request);
}
}
With [ApiController], invalid model state commonly results in an automatic 400 Bad Request response before the action body executes.
Minimal APIs do not behave exactly like controller model validation by default. In production Minimal APIs, validation is usually handled through endpoint filters, manual validation, FluentValidation, or a custom pipeline.
Binding Failure Behavior
Binding can fail when values are missing, malformed, or have the wrong content type.
Examples:
GET /api/products/not-an-int
If the endpoint expects int id, binding fails.
POST /api/products
Content-Type: application/json
{ invalid json }
If the endpoint expects a JSON body, deserialization fails.
Common results include 400 Bad Request for invalid values or invalid JSON, and 415 Unsupported Media Type when the request content type is wrong for body binding.
Good APIs should return consistent error responses. For public APIs, consider standard error shapes such as validation problem responses or Problem Details.
Overposting and DTO Safety
Overposting happens when a client sends fields that should not be controlled by the client, and the server accidentally binds them.
Risky DTO:
public sealed class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
public bool IsAdmin { get; set; }
}
[HttpPost("api/users")]
public IActionResult CreateUser([FromBody] User user)
{
// Dangerous if the client can set IsAdmin.
return Ok(user);
}
Safer request DTO:
public sealed class CreateUserRequest
{
public string Email { get; init; } = string.Empty;
}
[HttpPost("api/users")]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
var user = new User
{
Email = request.Email,
IsAdmin = false
};
return Ok(user);
}
Do not bind directly to database entities for create and update APIs. Use request DTOs that expose only the fields the client is allowed to send.
Choosing the Correct Binding Source
A practical design guide:
Example of a well-separated endpoint:
[HttpPut("api/products/{id:int}")]
public async Task<IActionResult> UpdateProduct(
[FromRoute] int id,
[FromQuery] bool publishImmediately,
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
[FromBody] UpdateProductRequest request,
[FromServices] IProductService productService)
{
await productService.UpdateAsync(id, request, publishImmediately, correlationId);
return NoContent();
}
Each value comes from a source that matches its purpose.
Common Mistakes
Common mistakes include:
- Relying on implicit binding when explicit attributes would make the API clearer.
- Putting optional filters in route segments instead of query string parameters.
- Binding directly to EF Core entities or domain entities.
- Using
[FromBody]for multiple parameters instead of creating one request DTO. - Expecting JSON body binding to work with
multipart/form-datarequests. - Trusting custom headers without validation or trusted infrastructure.
- Putting business logic inside custom binders.
- Forgetting that Minimal API and controller binding rules are similar but not identical.
- Ignoring model validation and assuming successful binding means valid input.
- Using services from request data accidentally because a Minimal API parameter type is registered in DI.
Best Practices
Use explicit binding attributes when endpoint readability matters.
public IActionResult Get(
[FromRoute] int id,
[FromQuery] bool includeDetails)
{
return Ok();
}
Use request DTOs instead of domain entities.
public sealed class UpdateProductPriceRequest
{
public decimal Price { get; init; }
}
Keep route parameters focused on identity.
/api/products/{id}
/api/customers/{customerId}/orders/{orderId}
Keep query parameters focused on filtering and optional behavior.
/api/products?search=mouse&page=1&pageSize=20
Use body parameters for complex commands.
[FromBody] CreateOrderRequest request
Use form parameters for file uploads and HTML form posts.
[FromForm] IFormFile file
Use headers for cross-cutting metadata.
[FromHeader(Name = "X-Correlation-Id")] string? correlationId
Use dependency injection for services, not request data.
[FromServices] IProductService productService
Validate after binding. Binding converts data; validation checks whether the data is acceptable.