Updated on

A GET request can carry parameters in three places: the query string, the URL path, and the request headers.

ASP.NET Core binds the query string and the route onto our action method’s parameters by name, so for those two we write an ordinary method signature and the framework fills it in. [FromQuery] and [FromRoute] exist for the cases where the name in the request and the name in the signature differ. A header is the exception: it binds only when we ask for it with [FromHeader].

The fourth place, the request body, is the one to avoid. A GET request may carry content, but HTTP gives that content no meaning, and some servers and proxies will reject the request outright.

To download the source code for the video, visit our Patreon page (YouTube Patron tier).

Before we dive into this topic, we recommend going through the basics of handling the GET request and best practices for ASP.NET Core Web API.


VIDEO: Pass Parameters With a GET Request in ASP.NET Core.


What Are the Parameters in a GET Request?

A GET request carries parameters in three places: the query string, the URL path, and the request headers.

The query string is everything after the question mark. It is a list of key=value pairs joined by &, so ?category=Electronic&brand=Sony passes two parameters. Their order does not matter and any of them can be left out.

The path carries parameters as segments. In /api/Product/2, the 2 is a parameter, and it forms part of the address of the resource rather than a filter applied to it.

Headers carry parameters outside the URL entirely. They never appear in an address bar, a browser history entry, a bookmark, or most server access logs, which is why credentials travel there.

A GET request may also carry a body, and the HTTP specification advises against relying on it. That leaves three usable places, and choosing between them comes down to what the value means rather than how big it is.

Setting Up the Application

To start with, we’ll create a model class, Product:

public class Product
{
    public int Id { get; set; }
    public string? Category { get; set; }
    public string? Brand { get; set; }
    public string? Name { get; set; }
    public int WarrantyYears { get; set; }
    public bool IsAvailable { get; set; }
}

Now, let’s create a ProductController class and define a list of products:

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly List<Product> _products = new()
    {
        new Product{ Id = 1, Category = "Electronic", Brand = "Sony",
                     Name = "Play Station", WarrantyYears = 2, IsAvailable = true },
        //removed other entires for a better readability
        new Product{ Id = 7, Category = "Electronic", Brand = "Apple",
                     Name = "Mobile", WarrantyYears = 2, IsAvailable = true }
    };
}

That is the whole setup. Each endpoint below reads its parameters, filters this list, and returns the matching products.

What Does the [HttpGet] Attribute Do?

[HttpGet] marks a controller method as the one that answers an HTTP GET request, and its argument supplies the route template that method answers on.

The template is relative to the controller’s own. [Route("api/[controller]")] on the class supplies the prefix, and [controller] expands to the class name with the Controller suffix removed, which is how ProductController ends up serving api/Product.

[HttpGet] with no argument answers that controller route, api/Product. [HttpGet("category")] appends a literal segment, giving api/Product/category. [HttpGet("{id}")] appends a placeholder, so api/Product/2 matches and the 2 binds to a parameter named id.

Literal segments beat placeholders when both could match a URL, so api/Product/category reaches the category action rather than the {id} one, even though both templates fit.

Whatever the method does not take from the route, it takes from the query string. That is why GetProductsByBrandAndWarranty(string brand, int warranty) on [HttpGet("brand/{brand}")] reads brand from the path and warranty from the query, with no attribute on either parameter.

Query Parameters

In a GET method, we commonly use query strings to pass parameters. We include them in the URL as key-value pairs separated by an equal sign and separate multiple parameters by an ampersand symbol.

Query parameters begin with a question mark (?), and we put them after the URL. If any route parameters exist in the URL, query parameters should be placed after them.

In this URL, category and brand are the query parameters with the values “Electronic” and “Sony”:

https://localhost:7097/api/Product?category=Electronic&brand=Sony

When we work with query parameters, we have the option to use the [FromQuery] attribute to bind parameters from the query string explicitly. However, the parameter binding process automatically converts request data into strongly typed parameters. This process eliminates the need for explicit [FromQuery] attribute usage. There is a lot more to say about that attribute than this article needs, and we cover it separately in our guide on reading values from the query string.

First, let’s create a GET method without using the [FromQuery] attribute:

[HttpGet]
public IActionResult GetProductsByCategoryAndBrand(string category, string brand)
{
    List<Product> result = _products
        .Where(x => x.Category == category && x.Brand == brand)
        .ToList();

    return Ok(result);
}

We have a GET endpoint that accepts two parameters category and brand. Then, we use the LINQ Where method to filter the _products based on the category and brand, and return the matches.

Let’s proceed by sending a request to the endpoint:

Pass two query prameters with a Get request in Postman

We can observe that parameter names in our method signature align with the query parameter names in the request URL, so the framework automatically binds the values from the query string to the method parameters based on their names and filters the _products. Turning that filtering into a real search feature is its own subject, which we walk through in our guide on searching a Web API with query parameters.

Sometimes, we may encounter situations where the parameter names in the method signature do not match the query parameter names. In such scenarios, we can explicitly use the [FromQuery] attribute to bind the query parameters to the respective method parameters.

Now, let’s create a GET method that takes two parameters from the query string, and we use the FromQuery attribute:

[HttpGet("type-manufacturer")]
public IActionResult GetProductsByCategoryAndBrandUsingFromQuery(
    [FromQuery(Name = "type")] string category,
    [FromQuery(Name = "manufacturer")] string brand)
{
    List<Product> result = _products
        .Where(x => x.Category == category && x.Brand == brand)
        .ToList();

    return Ok(result);
}

We use [FromQuery(Name = "type")] and [FromQuery(Name = "manufacturer")] attribute to bind the values of query parameters type and manufacturer to the corresponding method parameters category and brand.

To test this, let’s send a request with two query parameters:

Postman Result showing the API request and response.

The API filters the data and returns the result based on the query parameters type and manufacturer.

A query string is not unlimited. Kestrel caps the whole request line, method and URL together, at 8,192 bytes by default (Microsoft Learn, KestrelServerLimits.MaxRequestLineSize, read 2026-09-13), and browsers and reverse proxies impose their own smaller limits.

Additionally, query parameters are not secure because the values are visible in the URL, making them vulnerable to interception by malicious users.

Route Parameters

We can also use route parameters to pass data but in a different way. Route parameters are placeholders in the URL. We use route parameters to identify a specific resource or resources, while the query parameters to sort/filter those resources.

We recognize route parameters by curly braces ({}) containing a parameter name as part of the route template.

Let’s take a look at an URL that contains the route parameter:

https://localhost:7097/api/Product/2

We use this URL to access a product resource by passing the product Id as a route parameter to the API endpoint.

By default, the framework binds route parameters based on their names when we use route parameters, allowing us to omit the [FromRoute] attribute. However, there are scenarios where the parameter names in the method signature differ from the route parameter names. In such situations, we can use the [FromRoute] attribute.

Let’s create another GET method without using FromRoute attribute:

[HttpGet("{id}")]
public IActionResult GetProductById(int id)
{
    return Ok(_products
        .Where(x => x.Id == id)
        .FirstOrDefault());
}

Firstly, we define a GET method with a route parameter “id” using the [HttpGet("{id}")] attribute. Then, using the LINQ query, we filter the _products and return the product we find.

Writing the template as [HttpGet("{id:int}")] instead makes the route match only when the segment is a number, which keeps api/Product/abc from reaching this action at all. Making a segment optional rather than constrained is the next question about route templates, and we answer it in our guide on making a route parameter optional.

Let’s send a request with a route parameter:

Pass route parameters with a Get request in Postman

The API utilizes the route parameter to identify the specific resource.

Let’s create an additional GET method that uses [FromRoute] attribute:

[HttpGet("productId/{productId}")]
public IActionResult GetProductByIdUsingFromRoute([FromRoute(Name = "productId")] int id)
{
    return Ok(_products
        .Where(x => x.Id == id)
        .FirstOrDefault());
}

In this GET method, we define a [FromRoute(Name = "productId")] attribute to bind the productId parameter to the corresponding action method parameter.

Let’s send a request to the endpoint:

Postman Result showing the API request and response.

As we can see, API identifies the resource based on the route parameter we passed.

Combination of Query and Route Parameters

We can combine query and route params to pass params in a GET method. It is often more appropriate to pass a set of required parameters as route parameters and optional parameters as query parameters.

With this approach, we can design an API endpoint to include some of the parameters in the URL as route parameters while passing others in the URL as query parameters. The route parameters help identify the requested primary resource, while the query parameters help filter the result based on additional criteria.

Now, we’ll create a GET method that uses both query and route params:

[HttpGet("brand/{brand}")]
public IActionResult GetProductsByBrandAndWarranty(string brand, int warranty)
{
    return Ok(_products
        .Where(x => x.Brand == brand && x.WarrantyYears == warranty)
        .ToList());
}

The brand parameter is included in the URL as a route parameter while we pass the warranty as a query parameter. This allows retrieving _products by the brand while further filtering the result by the warranty.

We’ll send a request to the endpoint:

Postman Result showing the API request and response.

The API filters the _products based on the route and query parameters.

Now, let’s create a new GET action method that uses both the [FromQuery] and [FromRoute] attributes:

[HttpGet("manufacturer/{manufacturer}")]
public IActionResult GetProductsByBrandAndWarrantyUsingAttributes(
    [FromRoute(Name = "manufacturer")] string brand,
    [FromQuery(Name = "coverage")] int warranty)
{
    return Ok(_products
        .Where(x => x.Brand == brand && x.WarrantyYears == warranty)
        .ToList());
}

We have a GET endpoint that retrieves products based on the brand and warranty attributes. We use the [FromRoute(Name = "manufacturer")] attribute to ensure that the value for the brand parameter is retrieved from the route segment. Meanwhile, the [FromQuery(Name = "coverage")] attribute indicates that the warranty value will be obtained from the query string.

Let’s send a request:

Pass parameters with a Get request using Query and Route Parameters

By utilizing both the brand route parameter and the coverage query parameter, the API filters the _products.

Request Body

We typically use the request body for more complex or sensitive data that we cannot easily pass in the URL. It is commonly used in HTTP POST, PUT, and PATCH methods to send data to the server.

Do not do this in a GET method. RFC 9110 is direct about it: “content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request”. The example below shows how the binding works, because it is worth knowing why the request that “should” work sometimes does not, but it is not a pattern to copy.

By default, the HTTP methods GET, HEAD, OPTIONS, and DELETE do not bind data implicitly from the request body. To bind data explicitly from the request body, marked as JSON, for these methods, we can use the [FromBody] attribute. Also, we can use the [FromQuery] attribute as we did in our Pagination article.

For this example, we are going to create another GET method that accepts the request body using [FromBody] attribute:

[HttpGet("category")]
public IActionResult GetProductsByCategory([FromBody] Product model)
{
    return Ok(_products
        .Where(x => x.Category == model.Category)
        .ToList());
}

We use the route name “category” to indicate that we want to retrieve a list of _products belonging to a specific category. We utilize the Product class to pass the model into the request body. The [FromBody] attribute binds the request body values to the specified model.

Furthermore, we use LINQ to filter the _products based on their category and return them.

Let’s send a request to the endpoint:

Postman Result showing the API request and response.

The API filters the result based on the category parameter passed in the request body in JSON format.

When a GET action needs many values, the fix is not a body, it is a class. Marking a complex-type parameter [FromQuery] binds each of its properties from the query string, so one parameter object replaces eight method parameters and the request stays an ordinary GET.

Header Parameters

Header parameters are another way to pass data to a GET method using [FromHeader] attribute. Unlike query and route parameters, header parameters are not part of the URL. Instead, we can send them as part of the request headers.

Header parameters are an essential aspect of HTTP requests that provide additional information about the request. We can use header parameters to convey information such as authentication tokens, content type, and content-encoding.

Now, we’ll create a GET method that uses the header parameters:

[HttpGet("category-brand")]
public IActionResult GetProductsByCategoryAndBrandViaHeaders([FromHeader] string category, [FromHeader] string brand)
{
    return Ok(_products
        .Where(x => x.Category == category && x.Brand == brand)
        .ToList());
}

We use the [FromHeader] attribute to bind the category and brand header parameter values to the corresponding method parameters. We use “category-brand” as the route name because it filters the _products based on the provided category and brand. Then using the header parameters, we filter the _products using the LINQ Where() method and return them.

Using header parameters in GET requests is an excellent option to pass sensitive parameter values to avoid exposing them in the URL.

Let’s go ahead and send a request using the header parameters:

Postman Result when we pass parameters with a GET reqeust

When we send the API request, we pass the category and brand parameters in the headers, which the API uses to filter the results and return the _products.

Which Way Should We Use to Pass Parameters?

Use the route for what identifies the resource, the query string for what filters it, and headers for what should not appear in a URL.

The route answers “which one”. A product id belongs in the path because the path is the resource’s address, and api/Product/2 names exactly one thing.

The query string answers “which of them”. A category, a brand, a page number, a sort order: these are optional narrowings of a collection, and a query string is built for optional, order-independent values.

Headers answer “who is asking”. An API key or a tenant id is not part of what the reader is fetching, and it does not belong in a URL that gets bookmarked, logged, or pasted into a chat window.

Size rarely decides anything. A route and a query string share one request line, which Kestrel caps at 8,192 bytes by default, so a URL running out of room usually means the API’s shape is the real problem.

CarrierWhere it is in the requestBinding attributeDefault Kestrel budgetUse it for
Query stringafter the ? in the URL, as key=value pairs joined by &[FromQuery], optionalshares the 8,192-byte request linefiltering, sorting, paging, anything optional
Route segmentinside the URL path[FromRoute], optionalshares the 8,192-byte request lineidentifying one resource
Headerin the request headers, not in the URL[FromHeader], required32,768 bytes for all headersAPI keys, tokens, tenant ids
Request bodyafter the headers[FromBody], requirednot limited by the request linenothing (see RFC 9110)

Minimal APIs read the same three carriers with a different syntax, and we cover that side in our guide on the same parameters in a minimal API.

Conclusion

In this article, we discussed various ways of passing parameters to a GET method.

We commonly use query and route parameters to pass parameters in GET requests. However, if the parameter values are sensitive, header parameters or the request body may be more appropriate. We recommend avoiding passing data in the request body for GET requests and only considering it if other options are not feasible.

Once the values arrive, the next job is checking them, which we cover in our guide on validating the values that arrive.

Tested with .NET 10.0.10.