How to use parameter binding for routes in Minimal APIs

Published: Monday 18 August 2025

When discovering Minimal APIs, I was astounded how simple it was to create a route using one of the HTTP methods.

Now that I knew a bit about routing, I wanted to discover more. I started to look at parameter binding and this is what I found:

Parameter binding

With controllers, I have written many requests over the years that includes parameters in the route. Particularly the id parameter. And it works similar in Minimal APIs. If you have a parameter in your route, you can you use it as a binding source for your strongly typed parameters.

Here there is an {id} parameter in the route. This automatically binds to the int id parameter to be used in the endpoint:

// Program.cs
app.MapGet("/todo/{id}", (int id) =>
{
	return id;
});

I wondered what would happen if I was to apply an id to the query string? Something like /todo/2?id=4? In the response above, it would return 2. Turns out that having a route parameter is given a higher precedence than a query string parameter. If I wanted to explictly state that the id parameter comes from the querystring, I would have to remove {id} from the route.

// Program.cs
app.MapGet("/todo", (int id) =>
{
	return id;
});

But what if you want to specify the id in both the route and the query string? You can use the [FromRoute] and [FromQuery] attributes. And if the names are not a direct match, you can use the Name parameter in each of these attributes:

// Program.cs
app.MapGet("/todo/from/{id}", (
	[FromRoute(Name="id")] int routeId,
	[FromQuery(Name="id")] int queryId
) =>
{
	return $"Route Id = {routeId}, Query Id = {queryId}";
});

Here are the other [From*] attributes that you can use:

Name From attribute Notes
Route [FromRoute]
Query string [FromQuery]
Header [FromHeader] You must always include this attribute if the parameter is being binded from a request header key.
Body [FromBody] If you are using a GET, HEAD, OPTIONS or DELETE method, you must include the [FromBody] attribute
Form [FromForm]
Service [FromService] You don't normally need to include [FromService] when injecting from dependency injection.

I also came across [AsParameters]. What is that I wondered? Not come across that before? Turns out you can group your parameters into a class or a record type meaning you only need to pass in one parameter into the endpoint. You can even use any of the [From*] attributes to specify where the value comes from:

// ParametersDto.cs
public record ParametersDto(
    int Id, 
    int? Page, 
    string? Type, 
    [FromHeader(Name="api-key")] string? ApiKey
);
// Program.cs
app.MapGet("/todo/params/{id}", (
	[AsParameters] ParametersDto parameters
) => {
	return $"Id = {parameters.Id},\n" +
	$"Page = {parameters.Page},\n" +
	$"Type = {parameters.Type},\n" +
	$"Api key = {parameters.ApiKey}";
});

If you forget to add the [AsParameters] attribute, it will try to read it from the request body. If you are using a GET, HEAD, OPTIONS or DELETE method, you'll get the following exception when you run the application:

InvalidOperationException: 'Body was inferred but the method does not allow inferred body parameters.

Wildcard and route constraints

I wanted a way that you could add wildcard parameters in routes so I could capture the parameter including the forward slash. All you need to do is to add a * after the opening curly brace:

// Program.cs
app.MapGet("/todo/wildcard/{*slug}", (string slug) => slug);

When you run this endpoint with the route /todo/wildcard/show/the-full-path, the slug parameter will output show/the-full-path.

I also wanted to add a constraint to the route parameter. Constraints can be added with a : after the parameter name.

Here's how you would restrict the slug to certain characters using the regex constraint:

// Program.cs
app.MapGet("/todo/regex-wildcard/{*slug:regex(^[a-z0-9_-]+$)}",
	(string slug) => slug);

The regex constraint is just one of the route constraints that you can use in your endpoints.

Special types

When I use controllers, I often referenced the HttpContext instance. But, how would you do that with Minimal APIs?

Well they have a number of special types that you can pass in as parameters. Here is how you would use the HttpContext instance:

// Program.cs
app.MapGet("/hello-world",
    (HttpContext context) => context.Response.WriteAsync("Hello World"));

You can also use these types:

Special type Same as...
HttpContext
HttpRequest HttpContext.Request
HttpResponse HttpContext.Response
ClaimsPrincipal HttpContext.User
CancellationToken HttpContext.RequestAborted

Link generator

I also wanted the ability to generate a link from an existing route. That's where the LinkGenerator special type comes in. When you create a route, you can use the WithName extension method to specify the name.

Then when you want to reference that link in another route, you pass in the LinkGenerator type as a parameter and call the GetPathByName method.

// Program.cs
app.MapGet("/my-link", () => "My link").WithName("My link");
app.MapGet("/get-my-link", (LinkGenerator linkGenerator) =>
	$"The link to My Link is {linkGenerator.GetPathByName("My link")}");

Form values

There aren't many times that I need to pass in form values. These days I tend to create a class with properties I expect from a POST body request.

But if you are submitting a HTML form, then the IFormCollection will be of use to you. You can use the IFormCollection special type as a parameter in your route to capture form values. The IFormCollection has a list of KeyValuePair entires where you can get the key and value from each of your form values.

// Program.cs
app.MapPost("/form-values", (IFormCollection formCollection) =>
{
	return formCollection.Select(s => $"key = {s.Key}, value={s.Value}").ToList();
}).DisableAntiforgery();

File uploads

So far, minimal APIs has a lot of functionality that controllers does. But can it handle file uploads?

Well it turns out it can thanks to the IFormFile type. This example shows us uploading the file that is requested into the uploads directory:

// Program.cs
app.MapPost("/upload-file", async (IFormFile file) =>
{
	if (!Directory.Exists("uploads"))
	{
		Directory.CreateDirectory("uploads");
	}
	using var stream = File.OpenWrite($"uploads/{file.FileName}");
	await file.CopyToAsync(stream);
	return Results.NoContent();
}).DisableAntiforgery();

And if you are uploading multiple files in a request, you can use the IFormFileCollection type:

// Program.cs
app.MapPost("/upload-files", async (IFormFileCollection files) =>
{
	if (!Directory.Exists("uploads"))
	{
		Directory.CreateDirectory("uploads");
	}
	foreach (var file in files)
	{
		using var stream = File.OpenWrite($"uploads/{file.FileName}");
		await file.CopyToAsync(stream);
	}
	return Results.NoContent();
}).DisableAntiforgery();

We've disabled antiforgery for demonstrating purposes. However, antiforgery was a breaking change introduced to Minimal APIs in .NET 8 and it's recommended to be enabled. This is because it's a security precaution for APIs that consume data from a form.

Watch the video

To show you how simple and straight forward it is to set up parameters in Minimal APIs, I recorded this video where you can see how simple it is to use:

And rather than you having to type each example out, I'm sharing the code example so you can try it out.

Final thoughts

I like the fact that it shares a lot of common behaviours with controllers when it comes to parameter binding. It feels a lot more simpler than controllers. Less boiler plate code to write and much more readable.

I also like the fact that you can easily inject the HttpContext instance. This was something I used a lot with controllers so it was essential that it was easy to use with minimal APIs.