In our last tutorial we created an ASP.NET Core Minimal API from Start till Finish. We move further to understand how Parameter Bindings works in Minimal APIS.
Before we dive into parameter binding we have to understand what are routes and handlers.
In ASP.NET Core Minimal APIs, a route defines how an incoming HTTP request—identified by its URL path and HTTP method is mapped to a specific handler or delegate that contains the application’s business logic. Minimal APIs register endpoints directly on the WebApplication instance resulting in a lightweight, streamlined approach with minimal configuration and overhead.
Route handlers are methods that execute when the route matches. Route handlers can be a lambda expression, a local function, an instance method, or a static method.
// Example of lambda expression
app.MapGet("/example1", () => "This is lambda expression");
// Example of local function
string LocalFunction() => "This is local function";
app.MapGet("/example2", LocalFunction);
// Example of Instance method
app.MapGet("/example3", handler.Hello);
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
// Example of static method
app.MapGet("/", HelloHandler.Hello);
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
A Route parameter is a variable segment of a URL that allows values to be passed from the request URL to an endpoint, controller action, or Minimal API handler.
In the below example the route – “/products/books” has a single parameter called name. This route will return the message “The product is books”. In the same way when the route “/products/shoes” is called then it returns the message “The product is shoes”.
app.MapGet("/products/{name}", (string name) => $"The product is {name}");
In the below examples we have 2 route parameters countryName and cityName.
app.MapGet("/country/{countryName}/city/{cityName}", (string countryName, string cityName) => $"The country is {countryName} and city is {cityName}");
The route /country/India/city/Lucknow will return “The country is India and City is Lucknow”.
Route constraints are used to restrict which URLs match a Minimal API route by validating route parameter values. They help ensure that only requests with the correct format reach a specific action or endpoint.
In the below code we have added an int constraint to the “id” parameter so that only int values reaches it.
app.MapGet("/example/{id:int}", (int id) => $"The value of id is {id}");
The matching format for the above route – /example/1, /example/10, /example/99. The route /example/hello will not match since id value in the route is string (i.e. hello).
In the below case we have not applied any constraint.
app.MapGet("/example/{id}", (string id) => $"The value of id is {id}");
The matching format for the above route – /example/1, /example/hello, /example/hello99.
We can also use regex route constraint to match a route parameter against a regular expression. For example in the below code we restrict the following for the slug parameter.
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
The matching format in this case are:
The non-matching formats include.
The * character is used as a catch-all parameter. It matches the remainder of the URL path, including multiple path segments. The below example uses * to match all the remainder for the URL path after “match”.
app.MapGet("/match/{*slug}", (string slug) => $"Routing to {slug}");
Examples of formats matched in this case are.
Parameter binding is the process of mapping incoming request data to the strongly typed parameters defined by route handlers. A binding source specifies where the parameter values are obtained from. Binding sources can be explicitly defined or automatically inferred based on the HTTP method and the parameter type.
Supported binding sources:
In ASP.NET Core Minimal APIs, parameter binding follows a set of default rules to determine where each parameter value should come from. In most cases, you don’t need to specify attributes like [FromQuery] or [FromRoute].
Parameters are bound in the following order:
| Parameter type | Default binding source |
|---|---|
| Route parameter (name matches route template) | Route values |
| Simple types (int, string, bool, Guid, DateTime, etc.) not in route | Query String |
| Complex types | Request body in JSON |
| IFormFile, IFormFileCollection | Form data |
| Types registered in DI | Dependency Injection |
See the below endpoint.
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-MYCUSTOM-HEADER")] string customHeader,
Service service) => { });
In the above example of a GET Endpoint, the Parameters and their respective Binding Sources are given below. Here automatic parameter bindings are done by .NET as given in the below table.
| Parameter | Binding Source |
|---|---|
| id | Route |
| page | Query String |
| customHeader | Header by the name “X-MYCUSTOM-HEADER” |
| service | Dependency Injection |
By default, the GET, HEAD, OPTIONS, and DELETE HTTP methods do not bind parameters from the request body. To bind JSON data from the request body for these methods, explicitly use the [FromBody] attribute or read the body directly from the HttpRequest.
The HTTP POST method uses a default binding source of body (as JSON). In the below example the Employee object will be bind from the body.
app.MapPost("/", (Employee emp) => { });
See the below endpoint.
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromHeader(Name = "X-MYCUSTOM-HEADER")] string customHeader,
[FromServices] Service service) => { });
The above example does explicit parameter binding by the use of FromRoute, FromQuery, FromHeader, and FromServices attributes.
| Parameter | Binding Source |
|---|---|
| id | route value with the name id |
| page | query string with the name “p” |
| customHeader | Header by the name “X-MYCUSTOM-HEADER” |
| service | Dependency Injection |
| Special types like HttpContext, HttpRequest, HttpResponse, CancellationToken, ClaimsPrincipal | Automatically without explicit attributes |
The [FromForm] attribute binds form values explicitly as shown below. Note that if we don’t apply [FromForm] attribute then by default binding source of body (as JSON) is applied by .NET.
app.MapPost("/", ([FromForm] Employee emp) => { });
With the HttpRequest object we can read request data directly from the HTTP request. In ASP.NET Core, HttpRequest is a class that represents the incoming HTTP request sent by the client to your application. It provides access to information such as the request method, URL, headers, query string, form data, cookies, and request body.
See the below example.
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-MYCUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var emp = await request.ReadFromJsonAsync<Employee>();
// ...
});
The AsParameters is an attribute that groups multiple parameters into a single object while still binding each property from its appropriate source. This helps keep route handler signatures clean and organized.
For example, there is an endpoint containing multiple parameters.
app.MapGet("/products/{id}",
(int id, string? search, ILogger<Program> logger) =>
{
// ...
});
We can change this endpoint by using AsParameters attribute which will be binding a custom type called “ProductRequest” as shown below.
app.MapGet("/products/{id}", ([AsParameters] ProductRequest request) =>
{
// ...
});
Finally we can define a custom class called ProductRequest.cs containing all the parameters.
public class ProductRequest
{
public int Id { get; set; }
public string? Search { get; set; }
public ILogger<Program> Logger { get; set; } = default!;
}
Each property of the class is bound independently using the normal Minimal API binding rules:
We can override the default binding source by using attributes such as [FromRoute], [FromQuery], [FromHeader], [FromServices], and [FromForm]. See the below updated code of the class.
public class SearchRequest
{
[FromRoute]
public int Id { get; set; }
[FromQuery]
public string? Search { get; set; }
[FromHeader(Name = "X-Request-ID")]
public string? RequestId { get; set; }
[FromServices]
public ILogger<Program> Logger { get; set; } = default!;
}
Parameters declared in route handlers are considered required. A route handler is invoked only when the incoming request includes all required parameters. If any required parameter is missing, the request fails with an error instead of executing the handler.
We have an endpoint:
app.MapGet("/products", (int pageNumber) => $"Requested page {pageNumber}");
If we invoke the uri – /products then we get the error saying –
BadHttpRequestException: Required parameter "int pageNumber" wasn't provided from query string.
The solution to this is to make pageNumber optional, define the type as optional or provide a default value:
app.MapGet("/products", (int? pageNumber) => $"Requested page {pageNumber}");
app.MapGet("/products", (int pageNumber = 1) => $"Requested page {pageNumber}");
In Minimal APIs, parameters whose types are registered as services are automatically resolved through dependency injection. As a result, you don’t need to explicitly annotate them with the [FromServices] attribute. In the following example, both route handlers receive the service from the DI container and return the current time, even though only one uses [FromServices].
// Register the service
builder.Services.AddSingleton<TimeService>();
// Minimal API Endpoint
app.MapGet("/time", (TimeService timeService) => { return $"Current time: {timeService.GetCurrentTime()}"; });
// [FromServices] is optional
app.MapGet("/time", ([FromServices] TimeService timeService) => { return $"Current time: {timeService.GetCurrentTime()}"; });
// TimeService class
public class TimeService { public string GetCurrentTime() => DateTime.Now.ToString("T"); }
Special Types are bound automatically by .NET without explicit attributes.
* HttpContext : The context holds all the information of the current HTTP request or response.
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello Minimal API"));
* HttpRequest and HttpResponse : HTTP request and HTTP response.
app.MapGet("/", (HttpRequest request, HttpResponse response) =>
response.WriteAsync($"Hello Minimal API {request.Query["name"]}"));
* CancellationToken : cancellation token associated with the current HTTP request.
app.MapGet("/", async (CancellationToken cancellationToken) =>
await LongRunningRequestAsync(cancellationToken));
* ClaimsPrincipal : The user associated with the request, bound from HttpContext.User.
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
To upload files in a Minimal API, the request must use the multipart/form-data content type. Parameters of type IFormFile and supported named file collections such as IReadOnlyList
When the parameter type is IFormFileCollection, all uploaded files in the multipart/form-data request are bound to the collection, regardless of their form field names.
// Here we are using IFormFile so the parameter name in the route handler (file) must match the corresponding form field name in the request
app.MapPost("/upload", async (IFormFile file) =>
{
// The Path.GetTempFileName() creates a zero-byte temporary file in the operating system's default temporary directory which is C:\Users\<username>\AppData\Local\Temp\
var tempFile = Path.GetTempFileName();
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
// Here we are using IFormFileCollection so all uploaded files in the multipart/form-data request are bound to the collection, regardless of their form field names
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
IAntiforgery is used to generate and validate anti-forgery (CSRF) tokens. It helps protect web applications from Cross-Site Request Forgery (CSRF) attacks, where a malicious site tricks a user’s browser into submitting unwanted requests.
To implement anti-forgery token generate a form with an anti-forgery token and an /upload endpoint. In the /upload endpoint validates the anti-forgery token in the incoming request. If validation fails, an AntiforgeryValidationException is thrown and the request is rejected.
The below minimal api code generates and validates anti-forgery (CSRF) tokens.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg, .jpeg, .png" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
await antiforgery.ValidateRequestAsync(context);
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
app.Run();
If you run the above code a file upload form will be presented as shown by the below image:

When you view the page source, you can see the form’s code which is given below.
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8CJAiS5rYE9AjJoXkn5DPsi3_4TdjyD6twIrKrDao6kZK04ZNuy20TaQTatwOOD4G2HHrYE4QcNUajMDm-ecYOLGtK2jaQf5opiWXPn6CpBuSzkv0V8UDNkkWKcHLw_TjsuA6X5NKlEPakzpXAJA85k"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg, .jpeg, .png" />
<input type="submit" />
</form>
The anti-forgery token code is given inside the hidden input tag:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8CJAiS5rYE9AjJoXkn5DPsi3_4TdjyD6twIrKrDao6kZK04ZNuy20TaQTatwOOD4G2HHrYE4QcNUajMDm-ecYOLGtK2jaQf5opiWXPn6CpBuSzkv0V8UDNkkWKcHLw_TjsuA6X5NKlEPakzpXAJA85k"/>
Try uploading a file. When anti-forgery token validation is successful then the file is uploaded successfully and we get the message – “File uploaded successfully!”. Check the below image.

Invalid anti-forgery token will give the error:
CryptographicException: The payload was invalid. For more information go to https://aka.ms/aspnet/dataprotectionwarning
AntiforgeryValidationException: The antiforgery token could not be decrypted.
BadHttpRequestException: Invalid anti-forgery token found when reading parameter "IFormFile file" from the request body as form.
Check the below image where we have shown this error:

First the builder.Services.AddAntiforgery() method registers the antiforgery service in your ASP.NET Core dependency injection container. Then we need to tell the app to validate the antiforgery tokens. This is done by the code – app.UseAntiforgery().
The GetAndStoreTokens() method is the primary IAntiforgery method for generating anti-forgery tokens. It creates the tokens needed for CSRF protection and stores the cookie token in the response. A full HTML form is generated in the / endpoint which contains the token. This form is returned in the API response.
Next on the /upload endpoint, the token is validated with the code – await antiforgery.ValidateRequestAsync(context), and when the validation succeed then only the file is uploaded.
Here we will see an example that binds a multi-part form input to a complex object. We will also use antiforgery services for validation of antiforgery tokens.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html><body>
<form action="/job" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}" />
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="submit" />
</form>
</body></html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/job", async Task<Results<Ok<Work>, BadRequest<string>>>
([FromForm] Work work, HttpContext context, IAntiforgery antiforgery) =>
{
try
{
await antiforgery.ValidateRequestAsync(context);
return TypedResults.Ok(work);
}
catch (AntiforgeryValidationException e)
{
return TypedResults.BadRequest("Invalid antiforgery token");
}
});
app.Run();
class Work
{
public string Name { get; set; } = string.Empty;
public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}
The endpoint / will present a form with fields name and dueDate. We have to bind these fields to a Work.cs class.
The endpoint /job will bind the submitted form values to the Work class.
We have also used antiforgery services to support the generation and validation of antiforgery tokens.
In the below example binding to a complex type and a list of complex type is performed.
app.MapPost("/items", ([FromForm] List<Item> items) =>
{
return Results.Ok(items);
});
app.MapPost("/orders", ([FromForm] CreateOrderRequest request) =>
{
return Results.Ok(request);
});
public class CreateOrderRequest
{
public string Customer { get; set; } = "";
public List<Item> Items { get; set; } = [];
}
public class Item
{
public string Name { get; set; } = "";
public int Quantity { get; set; }
}
BindAsync is useful when:
This pattern is especially helpful for reusable request models like paging, filtering, search criteria, or authentication-related context.
The syntax is given below:
public static ValueTask<PagingData?> BindAsync(
HttpContext context,
ParameterInfo parameter)
This method is a special convention recognized by Minimal APIs. Whenever a parameter of type PagingData is needed, ASP.NET Core calls this method. Think of it as if the framework internally does:
PagingData pageData = await PagingData.BindAsync(context, parameter);
The parameter HttpContext context contains everything about the request. You can access.
context.Request.Query
context.Request.Headers
context.Request.RouteValues
context.Request.Body
context.Request.Form
The ParameterInfo parameter describes the endpoint parameter being bound. Here the pageData will provides metadata such as:
parameter.Name
parameter.ParameterType
parameter.Attributes
The following code displays SortBy:price, SortDirection:Desc, CurrentPage:10 with the URI /products?SortBy=price&SortDir=Desc&Page=10:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=price&SortDir=Desc&Page=10
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
The below code reads from the request body json and binds to a complex Work.cs class. The output returns the work class in json. See the below code.
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var work = await context.Request.ReadFromJsonAsync<Work>();
return Results.Ok(work);
}
else {
return Results.BadRequest();
}
});
class Work
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
If the request body contains the following JSON:
{"nameField":"Walk dog", "isComplete":false}
The endpoint returns the following JSON:
{
"name":"Walk dog",
"isComplete":false
}
Parameter binding is one of the core features that makes ASP.NET Core Minimal APIs both concise and powerful. Throughout this guide, you’ve seen how the framework automatically binds values from route parameters, query strings, headers, forms, services, and request bodies, as well as how to customize the binding process using attributes like FromRoute, FromQuery, FromHeader, FromForm, FromBody, FromServices, AsParameters, and the BindAsync convention for complex scenarios. Understanding these binding mechanisms allows you to design cleaner endpoints, reduce boilerplate code, and encapsulate request parsing logic into reusable models. By mastering parameter binding, you’ll be able to build Minimal APIs that are easier to read, maintain, and extend while taking full advantage of the flexibility and performance.