Skip to content

Query names, values and collections

Run the complete page example.

Search and list requests often need extra choices: a search term, a page number or a set of filters. A query string carries those values after the ? in a URL, such as ?page=1. Refit builds it from your method arguments.

You can keep simple arguments as they are, rename them for the service, or group related filters in an object. This page shows how Refit handles each form, including collections and missing values.

Build a search request

1. Describe the query inputs. SearchAsync keeps the fixed query active=true from its route. AliasAs gives name the service name q. Refit appends page and leaves out a null city. The other methods below show the query shapes used later on this page.

internal interface IQueryApi
{
    [Get("/people?active=true")]
    Task<HttpRequestMessage> SearchAsync([AliasAs("q")] string name, int page, string? city);

    [Get("/people")]
    Task<HttpRequestMessage> FilterAsync([Query(".", "filter")] SearchFilter filter);

    [Get("/people")]
    Task<HttpRequestMessage> CollectionsAsync([Query(CollectionFormat.Multi)] int[] ids, [Query(CollectionFormat.Csv)] string[] tags, [Query(CollectionFormat.Indexed)] List<Person> people);

    [Get("/people")]
    Task<HttpRequestMessage> FlagsAsync([QueryName] string?[] flags, [AliasAs("q")] [Encoded] string encoded);

    [Get("/people")]
    [QueryUriFormat(UriFormat.Unescaped)]
    Task<HttpRequestMessage> UnescapedAsync(string q);

    [Get("/people")]
    Task<HttpRequestMessage> JsonNamesAsync([Query] JsonNamedValue value);
}

2. Create the generated client. Use the shared client setup from the first request. The example creates api with RestService.ForGenerated<IQueryApi>(host.Client).

3. Inspect the query text. This return type builds a request without sending it. Refit escapes the space in the name as %20.

using HttpRequestMessage search = await api.SearchAsync("Ada Lovelace", 1, null);
Console.WriteLine(search.RequestUri); // /people?active=true&q=Ada%20Lovelace&page=1

Run the complete query example to check the scalar, object, collection, flag and URI-format URLs. The query-options example checks constructor choices, value formats and null handling.

Group values in an object

Refit can expand an object's public readable properties into query entries. This is called flattening. Query(".", "filter") adds filter. before each property name. The first argument chooses the text that joins the prefix and property name. The second argument chooses the prefix.

internal sealed class SearchFilter
{
    [AliasAs("name")]
    public string Term { get; init; } = "Ada Lovelace";

    public int PageSize { get; init; } = 20;

    [Query(Format = "0.00")]
    public decimal Price { get; init; } = 5;

    [Query(SerializeNull = true)]
    public string? Note { get; init; }
}

AliasAs keeps the explicit name name. SnakeCase changes PageSize to page_size. Query(Format = "0.00") gives the price two decimal places. SerializeNull = true sends the null note as note=. Without it, Refit leaves out a null property.

IQueryApi snakeApi = RestService.ForGenerated<IQueryApi>(host.Client, RefitSettings.SnakeCase());
using HttpRequestMessage grouped = await snakeApi.FilterAsync(new());
Console.WriteLine(grouped.RequestUri); // /people?filter.name=Ada%20Lovelace&filter.page_size=20&filter.price=5.00&filter.note=

Nested objects add another property name at each level. A dictionary with supported key and value types can also supply query entries. The generator follows the declared types when it builds these requests. For a value whose shape is known only at runtime, use a query converter.

Send a collection

Choose the collection format your service accepts. Multi repeats the key once per value. Csv joins values with commas. Indexed expands a collection of objects under keys such as people[0].Id. It applies to objects with public readable properties. A collection of simple values uses joined values instead.

using HttpRequestMessage collection = await api.CollectionsAsync([1, Second], ["math", "code"], [new(1, "Ada"), new(Second, "Grace")]);
Console.WriteLine(collection.RequestUri); // /people?ids=1&ids=2&tags=math%2Ccode&people[0].Id=1&people[0].Name=Ada&people[1].Id=2&people[1].Name=Grace

Second is the example's constant for ID 2.

CollectionFormatShape for two string values
Csvtags=math%2Ccode
Ssvtags=math%20code
Tsvtags=math%09code
Pipestags=math%7Ccode
Multitags=math&tags=code
IndexedObject properties such as people[0].Id=1&people[1].Id=2.
RefitParameterFormatterUses the configured formatter. The default query formatter joins values with commas.

An explicit Query(CollectionFormat.Multi) overrides RefitSettings.CollectionFormat. When the attribute does not select a format, the setting supplies it. An empty joined collection sends key=. An empty Multi collection sends no entries. Multi skips null elements. Joined formats keep an empty place for each null element.

For a service whose wire names differ from your C# names, put AliasAs on the query properties and choose CollectionFormat.Multi for repeated filters. This keeps the request model readable while producing the exact keys and repeated values the service expects.

Send flags and already escaped text

QueryName uses the argument's value as a query name with no =value. A collection produces one flag for each non-null element.

Encoded tells Refit that the argument is already URL-escaped. Use it when you already have valid escaped text. Keep the default escaping for ordinary input. It applies to path arguments, query values and query flags.

using HttpRequestMessage flagged = await api.FlagsAsync(["preview", null, "include notes"], "Ada%20Lovelace");
Console.WriteLine(flagged.RequestUri); // /people?preview&include%20notes&q=Ada%20Lovelace

QueryUriFormat chooses how .NET renders the final path and query. For example, UriFormat.Unescaped keeps a space in the built request's original URI text. It affects the whole path and query, including values you marked Encoded. Use it only when you need that final URI format.

using HttpRequestMessage unescaped = await api.UnescapedAsync("Ada Lovelace");
Console.WriteLine(unescaped.RequestUri?.OriginalString); // /people?q=Ada Lovelace

Query attribute choices

Attribute or propertyUse
AliasAs(name) / NameSets an explicit parameter or property name.
Query()Keeps the default delimiter and the configured collection format.
Query(delimiter) / DelimiterChooses the text between nested names. The default is ..
Query(delimiter, prefix) / PrefixAdds a name before flattened properties.
Query(delimiter, prefix, format) / FormatAlso supplies a value format string.
Query(collectionFormat) / CollectionFormatSelects a collection format for this argument.
Query.IsCollectionFormatSpecifiedTells custom code whether the attribute explicitly chose a collection format.
Query.TreatAsStringUses the object's ToString() result instead of flattening its properties.
Query.SerializeNullSends a null property as an empty value.
QueryName()Sends valueless flags.
Encoded()Keeps caller-escaped text.
QueryUriFormat(uriFormat) / UriFormatSets the final path and query rendering mode.

For shared naming and value rules, see query formatters.

The complete query API reference is:

MemberDescriptionParametersReturns or value
CollectionFormatSelects how a collection becomes query or form text.None.enum with the values listed above.
CollectionFormat.RefitParameterFormatterDelegates collection rendering to the configured URL or form formatter.None.int value 0; the default enum value.
CollectionFormat.CsvJoins values with a comma.None.int value 1.
CollectionFormat.SsvJoins values with a space.None.int value 2.
CollectionFormat.TsvJoins values with a tab.None.int value 3.
CollectionFormat.PipesJoins values with a pipe character.None.int value 4.
CollectionFormat.MultiEmits one key-value pair for each collection value.None.int value 5.
CollectionFormat.IndexedExpands each object element under an indexed key such as items[0].Name.None.int value 6; scalar elements use comma-separated values.
AliasAsAttributeAn attribute that replaces a query parameter or property name with a service-specific name.Applied to a parameter or property.Sealed Attribute type.
AliasAsAttribute(string name)Marks a parameter or property with the exact name Refit sends on the wire.string name: wire name.An attribute whose Name replaces the CLR name.
AliasAsAttribute.NameReturns the alias supplied to the constructor.None. Read-only.string wire name.
EncodedAttributeAn attribute that tells generated request building to preserve a caller-encoded parameter.Applied to a parameter.Sealed Attribute type.
EncodedAttribute()Marks a parameter value as URL-encoded text that Refit appends verbatim.None.Attribute for path segments, query values, and QueryName flags.
QueryAttributeAn attribute that controls query or form field names, scalar formats, and collection formats.Applied to a parameter or property.Sealed Attribute type.
QueryAttribute()Uses . as the nested-name delimiter and leaves the collection format to client settings.None.Attribute with no prefix or value format.
QueryAttribute(CollectionFormat collectionFormat)Selects a collection format for this parameter or property.CollectionFormat collectionFormat: explicit collection mode.Attribute for which IsCollectionFormatSpecified is true.
QueryAttribute(string delimiter)Changes the separator between names when Refit flattens a complex value.string delimiter: nested-name separator.Attribute with the supplied delimiter.
QueryAttribute(string delimiter, string prefix)Changes flattened names to prefix + delimiter + propertyName.string delimiter: nested-name separator; string prefix: name before flattened properties.Attribute with the supplied delimiter and prefix.
QueryAttribute(string delimiter, string prefix, string format)Also stores a value format for a scalar query value. It does not apply that format to flattened properties.string delimiter: nested-name separator; string prefix: name before flattened properties; string format: value format string.Attribute with the supplied delimiter, prefix, and format.
QueryAttribute.CollectionFormatGets the selected format, or sets an explicit format that overrides client settings.None.CollectionFormat; reads as RefitParameterFormatter until set, while IsCollectionFormatSpecified distinguishes that unset state.
QueryAttribute.DelimiterReturns the separator that joins the prefix and flattened property name.None. Read-only.string, default ".".
QueryAttribute.FormatGets or sets the format string for a scalar query value.None.string or null; default null.
QueryAttribute.IsCollectionFormatSpecifiedReports whether code assigned CollectionFormat, including through the collection-format constructor.None. Read-only.bool, default false.
QueryAttribute.PrefixReturns the name prepended to each flattened property.None. Read-only.string or null; default null.
QueryAttribute.SerializeNullControls whether a null property is written as an empty value instead of omitted.None.bool, default false.
QueryAttribute.TreatAsStringControls whether Refit uses an object's ToString() result instead of flattening its properties.None.bool, default false.
QueryNameAttributeAn attribute that creates a presence-style query flag from a parameter value.Applied to a parameter.Sealed Attribute type.
QueryNameAttribute()Marks a parameter whose formatted value becomes a bare query flag without =value.None.Attribute that omits null values and renders collection elements as separate flags.
QueryUriFormatAttributeAn attribute that controls how .NET renders a method's final request URI.Applied to a method.Sealed Attribute type.
QueryUriFormatAttribute(UriFormat uriFormat)Sets the .NET URI rendering mode for the method's complete path and query.UriFormat uriFormat: final URI rendering mode.Attribute applied to a method.
QueryUriFormatAttribute.UriFormatReturns the URI rendering mode supplied to the constructor.None. Read-only.UriFormat.

Production source: AliasAsAttribute.cs, CollectionFormat.cs, EncodedAttribute.cs, QueryAttribute.cs, QueryNameAttribute.cs, and QueryUriFormatAttribute.cs.

Compare constructor and format choices

The five constructors store the delimiter, prefix, value format or explicit collection format. The default attribute's collection-format property reads RefitParameterFormatter, but IsCollectionFormatSpecified is false, so the client's setting still decides the format. Setting CollectionFormat, including through its constructor, makes that flag true.

QueryAttribute defaults = new();
QueryAttribute delimiter = new("-");
QueryAttribute prefixed = new("-", Prefix);
QueryAttribute formatted = new("-", Prefix, "yyyy-MM");
QueryAttribute repeated = new(CollectionFormat.Multi) { SerializeNull = true, TreatAsString = true };
Console.WriteLine(defaults.Delimiter); // .
Console.WriteLine(defaults.Prefix is null); // True
Console.WriteLine(defaults.IsCollectionFormatSpecified); // False
Console.WriteLine(delimiter.Delimiter); // -
Console.WriteLine(prefixed.Prefix); // filter
Console.WriteLine(formatted.Format); // yyyy-MM
Console.WriteLine(repeated.CollectionFormat); // Multi
Console.WriteLine(repeated.IsCollectionFormatSpecified); // True

The three-argument Query formats a scalar value such as amount below; its prefix and delimiter do not rename that scalar key. On the object argument, it changes the names but its format is not applied to the object's properties. Started therefore keeps its default invariant date text; End uses its own Query(Format = "yyyy") attribute. Put formats on properties, or use a container rule from query formatters.

internal interface IQueryOptionsApi
{
    [Get("/reports")]
    Task<HttpRequestMessage> DatesAsync([Query("-", "filter", "yyyy-MM")] DateFilter dates);

    [Get("/reports")]
    Task<HttpRequestMessage> AmountAsync([Query("-", "filter", "0.00")] decimal amount);

    [Get("/reports")]
    Task<HttpRequestMessage> TagsAsync(string?[] tags);

    [Get("/reports")]
    Task<HttpRequestMessage> TextAsync([Query(TreatAsString = true)] SearchText phrase, [Query(Format = "")] SearchText other);
}
IQueryOptionsApi api = RestService.ForGenerated<IQueryOptionsApi>(host.Client, host.Settings);
DateTime day = DateTime.ParseExact("2026-09-17", "yyyy-MM-dd", CultureInfo.InvariantCulture);
using HttpRequestMessage dates = await api.DatesAsync(new() { Started = day, End = day });
Console.WriteLine(dates.RequestUri); // /reports?filter-Started=09%2F17%2F2026%2000%3A00%3A00&filter-End=2026
using HttpRequestMessage amount = await api.AmountAsync(1);
Console.WriteLine(amount.RequestUri); // /reports?amount=1.00

TreatAsString = true calls the object's ToString() before value formatting. An explicitly empty Format = "" selects the same behavior in generated requests. A whitespace format does not select that shortcut. Define ToString to return the service's text:

internal sealed record SearchText(string Value)
{
    public override string ToString() => Value;
}
using HttpRequestMessage text = await api.TextAsync(new("Ada Lovelace"), new("Grace Hopper"));
Console.WriteLine(text.RequestUri); // /reports?phrase=Ada%20Lovelace&other=Grace%20Hopper

This loop builds each scalar collection format with a null middle element. Joined formats retain its empty place. Indexed uses comma-separated values for this scalar collection; the object collection earlier on this page demonstrates indexed property names. results stores the paths for the full sample's assertions.

CollectionFormat[] formats =
[
    CollectionFormat.RefitParameterFormatter, CollectionFormat.Csv,
    CollectionFormat.Ssv, CollectionFormat.Tsv, CollectionFormat.Pipes,
    CollectionFormat.Multi, CollectionFormat.Indexed,
];
List<(CollectionFormat Format, string? Path)> results = [];
foreach (CollectionFormat format in formats)
{
    RefitSettings settings = new(host.Settings.ContentSerializer) { CollectionFormat = format };
    IQueryOptionsApi api = RestService.ForGenerated<IQueryOptionsApi>(host.Client, settings);
    using HttpRequestMessage request = await api.TagsAsync(["math", null, "code"]);
    Console.WriteLine($"{format}: {request.RequestUri}");
    results.Add((format, request.RequestUri?.OriginalString));
}

Attribute constructors store their supplied strings without validating them. Use a deliberate delimiter and prefix; a null prefix means no prefix. Leaving out a value format means the normal formatter rules apply.