DEV_NET_CORE
GET_STARTED
.NETAPI design and implementation

Parameter Binding

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:

Code
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:

Code
[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:

SourceAttributeCommon Use
Route values[FromRoute]Resource identifiers such as /products/{id}
Query string[FromQuery]Filtering, sorting, paging, search terms
Body[FromBody]JSON request payloads for create/update operations
Form[FromForm]HTML forms, multipart/form-data, file uploads
Header[FromHeader]Correlation IDs, tenant IDs, version hints, conditional request headers
Services[FromServices]Dependency injection services used by the endpoint

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:

Code
[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:

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

Code
[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:

Code
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:

Code
GET /api/products/category/electronics/min-price/100/max-price/500/sort/name

Better:

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

Code
[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:

Code
GET /api/products?search=keyboard&page=2&pageSize=10&sort=price_desc

For many query parameters, a request object is cleaner:

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

Code
[HttpGet("api/products/by-tags")]
public IActionResult GetByTags([FromQuery] string[] tags)
{
    return Ok(tags);
}

Possible request:

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

Code
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:

Code
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:

Code
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:

Code
[HttpPost("api/orders")]
public IActionResult CreateOrder(
    [FromBody] CustomerDto customer,
    [FromBody] OrderDto order)
{
    return Ok();
}

Prefer one request DTO:

Code
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:

Code
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:

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

Code
[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:

HeaderPurpose
AuthorizationAuthentication credential, usually handled by authentication middleware
X-Correlation-IdRequest tracing across services
X-Tenant-IdTenant identification in multi-tenant systems, if appropriate for the architecture
If-MatchOptimistic concurrency with ETags
Accept-LanguagePreferred language or culture

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:

Code
[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:

Code
[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:

Code
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:

Code
[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:

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:

  1. Use explicit attributes first, such as [FromRoute], [FromQuery], [FromHeader], [FromBody], [FromForm], [FromServices], or [AsParameters].
  2. Bind special framework types directly, such as HttpContext, HttpRequest, HttpResponse, ClaimsPrincipal, CancellationToken, IFormFile, Stream, or PipeReader.
  3. Use custom binding methods like BindAsync when available.
  4. Use TryParse for simple parseable values from route or query.
  5. Use dependency injection if the parameter type is registered as a service.
  6. 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:

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

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

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

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

Code
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:

Code
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]:

Code
[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:

Code
GET /api/products/not-an-int

If the endpoint expects int id, binding fails.

Code
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:

Code
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:

Code
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:

DataRecommended SourceExample
Resource identityRoute/api/products/{id}
Optional filtersQuery string?category=books&page=2
Create/update commandBodyJSON DTO
File uploadFormmultipart/form-data with IFormFile
Request metadataHeaderX-Correlation-Id
Application dependencyServicesIProductService

Example of a well-separated endpoint:

Code
[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-data requests.
  • 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.

Code
public IActionResult Get(
    [FromRoute] int id,
    [FromQuery] bool includeDetails)
{
    return Ok();
}

Use request DTOs instead of domain entities.

Code
public sealed class UpdateProductPriceRequest
{
    public decimal Price { get; init; }
}

Keep route parameters focused on identity.

Code
/api/products/{id}
/api/customers/{customerId}/orders/{orderId}

Keep query parameters focused on filtering and optional behavior.

Code
/api/products?search=mouse&page=1&pageSize=20

Use body parameters for complex commands.

Code
[FromBody] CreateOrderRequest request

Use form parameters for file uploads and HTML form posts.

Code
[FromForm] IFormFile file

Use headers for cross-cutting metadata.

Code
[FromHeader(Name = "X-Correlation-Id")] string? correlationId

Use dependency injection for services, not request data.

Code
[FromServices] IProductService productService

Validate after binding. Binding converts data; validation checks whether the data is acceptable.

Interview Practice

PreviousOpenAPI generation and API discoverabilityNext UpAuthentication vs Authorization in C#