Refit.HttpClientFactory
15.2.0
Prefix Reserved
Requires NuGet 2.12 or higher.
dotnet add package Refit.HttpClientFactory --version 15.2.0
NuGet\Install-Package Refit.HttpClientFactory -Version 15.2.0
<PackageReference Include="Refit.HttpClientFactory" Version="15.2.0" />
<PackageVersion Include="Refit.HttpClientFactory" Version="15.2.0" />
<PackageReference Include="Refit.HttpClientFactory" />
paket add Refit.HttpClientFactory --version 15.2.0
#r "nuget: Refit.HttpClientFactory, 15.2.0"
#:package Refit.HttpClientFactory@15.2.0
#addin nuget:?package=Refit.HttpClientFactory&version=15.2.0
#tool nuget:?package=Refit.HttpClientFactory&version=15.2.0
Refit: The automatic type-safe REST library for modern .NET
| Refit | Refit.HttpClientFactory | Refit.Newtonsoft.Json | Refit.Testing | |
|---|---|---|---|---|
| NuGet |
Refit is a library heavily inspired by Square's Retrofit library, and it turns your REST API into a live interface:
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<User> GetUser(string user);
}
The RestService class generates an implementation of IGitHubApi that uses
HttpClient to make its calls:
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
var octocat = await gitHubApi.GetUser("octocat");
.NET supports registering Refit clients via HttpClientFactory:
services
.AddRefitClient<IGitHubApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));
To test the clients you build with Refit, the Refit.Testing package
lets you stub responses and verify requests with a declarative route table — see
Testing your Refit clients.
Table of Contents
- Sponsors
- Where does this work?
- SDK Requirements
- Breaking changes and release notes
- Source generation
- API Attributes
- Querystrings
- Body content
- Setting request headers
- Passing state into DelegatingHandlers
- Per-request timeouts
- Multipart uploads
- Retrieving the response
- Using generic interfaces
- Interface inheritance
- Default Interface Methods
- Using HttpClientFactory
- Providing a custom HttpClient
- Handling exceptions
- When returning Task<IApiResponse>, Task<IApiResponse<T>>, or Task<ApiResponse<T>>
- When returning Task<T>
- Inspecting the error body synchronously
- Reading the request body that was sent
- Providing a custom ExceptionFactory
- Providing a custom TransportExceptionFactory
- ApiException deconstruction with Serilog
- Testing your Refit clients
Sponsors
Refit is sponsored by the following:
Where does this work?
Refit currently supports the following platforms and modern .NET targets:
- WinUI
- Desktop .NET Framework 4.6.2+
- .NET 8 / 9 / 10 / 11
- Blazor
- Uno Platform
SDK Requirements
The source generator ships inside the Refit package as a Roslyn analyzer, so it is your build tools,
not your target framework, that decide whether you get generated clients:
| Requirement | Minimum |
|---|---|
| Roslyn (C# compiler) | 4.8 |
| Visual Studio | 2022 17.8 |
| .NET SDK | 8.0.100 |
| NuGet references | PackageReference |
These are build-time requirements only. The compiled output still runs on every platform listed above, down
to .NET Framework 4.6.2, and both SDK-style and legacy (non-SDK) .csproj projects are supported.
Older build tools do not silently produce an empty client — the build fails with REFIT001 naming the Roslyn
version it found. packages.config cannot load analyzers at all, so a source generator will never run there;
migrate to PackageReference
to use it.
If you cannot move to these build tools, set DisableRefitSourceGenerator to true and add the
Refit.Reflection package, which builds requests at runtime
and needs no source generator.
Breaking changes and release notes
Breaking changes and the notable additions for each major version — including the V14 move of the reflection
request builder into the opt-in Refit.Reflection package —
are documented in Breaking changes and release notes.
Source generation
The Refit package ships Roslyn source generators. A PackageReference to Refit gets you generated clients at build
time — no extra package.
The generated code targets C# 7.3. On C# 8 or newer, the generator also emits nullable directives and annotations.
You create generated clients with the normal APIs:
var api = RestService.For<IGitHubApi>("https://api.github.com");
or through Refit.HttpClientFactory:
services
.AddRefitClient<IGitHubApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));
Generated clients use the same RefitSettings you pass to RestService.For<T> or AddRefitClient<T>, and honor settings
such as:
ContentSerializerUrlParameterFormatterUrlParameterKeyFormatterCollectionFormatAuthorizationHeaderValueGetterExceptionFactoryDeserializationExceptionFactoryTransportExceptionFactoryHttpRequestMessageOptionsVersionandVersionPolicy
Automatic client registration
On .NET 5 and newer the generator emits a module initializer
that registers every generated client factory at assembly load. RestService.ForGenerated<T> and
AddRefitGeneratedClient<T> then resolve the client with no runtime reflection. RestService.For<T> skips the reflection
request builder for fully generated interfaces. That keeps trimmed and Native AOT apps reflection-free.
.NET Framework (net462–net481) gets no automatic registration. ModuleInitializerAttribute arrived in .NET 5 and
isn't in the .NET Framework BCL, so Refit emits the initializer only for .NET 5+ targets. Raising a project's
<LangVersion> to 9 doesn't change that — the missing type is the blocker, not the language version. (It does switch on
modern generated syntax like nullable annotations.)
The generated inline code still runs. But RestService.For<T> finds the client by runtime type lookup and builds the
reflection request builder, so you must reference the opt-in Refit.Reflection
package. .NET Framework has no trimming or AOT, so this costs nothing there.
ForGenerated<T> and AddRefitGeneratedClient<T> need that registration. On .NET Framework they throw unless you register
the factory yourself with RegisterGeneratedFactory<T> / RegisterGeneratedSettingsFactory<T> at startup.
Generated-only client creation
For Native AoT or trimmed apps, RestService.ForGenerated<T> creates a client only when the generator registered an
implementation for that interface:
using var client = new HttpClient
{
BaseAddress = new Uri("https://api.github.com")
};
var api = RestService.ForGenerated<IGitHubApi>(client);
ForGenerated<T> never falls back to the reflection client. When the generated client builds every request directly,
Refit skips the reflection request builder too. If no generated implementation is registered, it throws an
InvalidOperationException pointing back to source generation setup.
Generated request building
By default the generator builds requests directly. Instead of a method body that calls the reflective pipeline through
BuildRestResultFuncForMethod, the generated client creates the HttpRequestMessage, applies headers, properties, and
body, then dispatches it through Refit's runtime helpers.
This cuts runtime reflection, metadata lookup, argument boxing, and delegate construction. It covers most request shapes:
- parameters that appear in the path template
- query parameters: auto-appended,
[AliasAs],[Query](includingFormatandCollectionFormat), scalar collections,[QueryName]flags and[Encoded]values - implicit
[Body]detection on POST/PUT/PATCH - static
[Headers] - dynamic
[Header]parameters [HeaderCollection]dictionaries[Body]content[Multipart]form uploads whose parts areStreamPart/ByteArrayPart/FileInfoPart(or aMultipartItemsubclass),Stream,string,FileInfo,byte[],HttpContent, a date/time orGuidvalue, or an enumerable of these (an object part serialized through the content serializer still falls back to the runtime builder)[Property]parameters[Property]interface properties- cancellation tokens
Task,Task<T>,Task<ApiResponse<T>>, and related response wrappersIAsyncEnumerable<T>for streamed responses
For a shape the generator can't emit yet, that method falls back to the runtime request builder.
Turn generated request building off in a project file:
<PropertyGroup>
<RefitGeneratedRequestBuilding>false</RefitGeneratedRequestBuilding>
</PropertyGroup>
That keeps the generated interface implementation but routes its methods through the reflective request builder.
Disable source generation entirely:
<PropertyGroup>
<DisableRefitSourceGenerator>true</DisableRefitSourceGenerator>
</PropertyGroup>
Most applications should leave both settings unset.
Analyzer diagnostics and code fixes
The main package also ships analyzers. They flag common interface issues at compile time:
- methods or properties on Refit interfaces that cannot be generated or called by Refit
- route templates that use backslashes instead of forward slashes
- methods with more than one
CancellationTokenparameter [HeaderCollection]parameters that are notIDictionary<string, string>- methods with more than one
[Body]parameter [Multipart]methods that also declare a[Body]parameter
Mechanical fixes ship as code fixes: replacing route backslashes with forward slashes, and changing an invalid
[HeaderCollection] parameter to IDictionary<string, string>.
API Attributes
Every method must have an HTTP attribute that provides the request method and relative URL. There are six built-in annotations: Get, Post, Put, Delete, Patch and Head. The relative URL of the resource is specified in the annotation.
[Get("/users/list")]
You can also specify query parameters in the URL:
[Get("/users/list?sort=desc")]
A request URL can be updated dynamically using replacement blocks and parameters on the method. A replacement block is an alphanumeric string surrounded by { and }.
If the name of your parameter doesn't match the name in the URL path, use the
AliasAs attribute.
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId);
A request url can also bind replacement blocks to a custom object
[Get("/group/{request.groupId}/users/{request.userId}")]
Task<List<User>> GroupList(UserGroupRequest request);
class UserGroupRequest{
int groupId { get;set; }
int userId { get;set; }
}
When the bound object is a generic method's type parameter, the placeholders are
resolved against a class constraint at compile time, so a constrained generic
method is generated inline (no reflection fallback, no RF006):
// Generated inline: {request.groupId}/{request.userId} bind against UserGroupRequest.
[Get("/group/{request.groupId}/users/{request.userId}")]
Task<List<User>> GroupList<T>(T request) where T : UserGroupRequest;
An unconstrained generic parameter (GroupList<T>(T request)) still falls back
to the reflection request builder, because the concrete type - and its bound
properties - are only known at call time.
Parameters that are not specified as a URL substitution will automatically be used as query parameters. This is different than Retrofit, where all parameters must be explicitly specified.
The comparison between parameter name and URL parameter is not
case-sensitive, so it will work correctly if you name your parameter groupId
in the path /group/{groupid}/show for example.
[Get("/group/{groupid}/users")]
Task<List<User>> GroupList(int groupId, [AliasAs("sort")] string sortOrder);
GroupList(4, "desc");
>>> "/group/4/users?sort=desc"
Round-tripping route parameter syntax: Forward slashes aren't encoded when using a double-asterisk (**) catch-all parameter syntax.
During link generation, the routing system encodes the value captured in a double-asterisk (**) catch-all parameter ( for example, {**myparametername}) except the forward slashes.
The type of round-tripping route parameter must be string.
[Get("/search/{**page}")]
Task<List<Page>> Search(string page);
Search("admin/products");
>>> "/search/admin/products"
Optional route parameters: append ? to a placeholder name ({name?}, matching ASP.NET routing) to make the segment
optional. When the bound argument is null the segment and the slash in front of it are dropped, so the URL never
gains a trailing or doubled slash. A non-null value (including an empty string) formats exactly like a normal {name}
placeholder.
[Get("/push/notifMsg/{deviceId}/{notifMsgId?}")]
Task<string> PushMessage(string deviceId, string? notifMsgId);
PushMessage("device1", "msg42");
>>> "/push/notifMsg/device1/msg42"
PushMessage("device1", null);
>>> "/push/notifMsg/device1" // the trailing segment and its '/' are dropped, so it will not 404
Optional applies to a segment: an interior {name?} (for example /a/{first?}/b) collapses to /a/b when null rather
than leaving /a//b, and a dotted object placeholder can be optional too ({repo.Name?}). In a query position
(?key={value?}) there is no preceding slash to trim, so a null value simply renders an empty value like a normal null.
The behaviour is identical on the reflection and source-generated request paths.
By default Refit throws if a route template contains a placeholder with no matching method argument. If you want to
resolve a placeholder later yourself (for example an API-versioning token rewritten inside a DelegatingHandler), set
AllowUnmatchedRouteParameters on RefitSettings. The unmatched {token} is then left in the URL verbatim instead of
throwing.
var settings = new RefitSettings { AllowUnmatchedRouteParameters = true };
var api = RestService.For<IVersionedApi>("https://api.example.com", settings);
// [Get("/api/{version:apiVersion}/values")]
// the {version:apiVersion} token is left in the path for a DelegatingHandler to replace.
Base address and URL resolution
By default Refit requires relative paths to start with /, prepends the base address path itself, and trims a trailing
slash from the base address. If you would rather have the base address and relative URL combined the same way
HttpClient and System.Uri do (RFC 3986), set UrlResolution on RefitSettings:
var settings = new RefitSettings { UrlResolution = UrlResolutionMode.Rfc3986 };
var api = RestService.For<IMyApi>("https://api.example.com/api/v1/", settings);
Under UrlResolutionMode.Rfc3986 the leading-slash requirement is relaxed and the trailing slash on the base address is
significant, exactly as with HttpClient:
// base address "https://api.example.com/api/v1/"
[Get("values")] // -> https://api.example.com/api/v1/values (appended)
[Get("/values")] // -> https://api.example.com/values (leading slash replaces the base path)
Note: with generated request building (the default), a leading-slash-less route under the default legacy resolution is validated when the request is built — so the
ArgumentExceptionsurfaces on the first call rather than fromRestService.For<T>(...). UnderUrlResolutionMode.Rfc3986the route is valid and no exception is raised.
Absolute URLs per call with [Url]
The route templates and {**catch-all} segments above build a path relative to the client's base address. When you
instead need to dispatch a single call to an arbitrary absolute URL — often a different host, such as a
pre-signed download link or a URL returned by a previous response — mark a string or System.Uri parameter with
[Url]. Its value becomes the request URI and the client's base address is ignored. This is Refit's equivalent of
Retrofit's @Url.
public interface IFileApi
{
[Get("")]
Task<Stream> Download([Url] string absoluteUrl);
}
// base address "https://api.example.com" is ignored:
api.Download("https://cdn.example.com/files/report.pdf");
>>> GET https://cdn.example.com/files/report.pdf
- The value must be an absolute URI; a relative or otherwise invalid value throws an
ArgumentExceptionwhen the request is built. - Because
[Url]supplies the full URL, the method's route template must be empty ([Get("")]). Combining[Url]with a non-empty path template throws anArgumentException. [Query]parameters still work and are appended to the absolute URL's query string:
[Get("")]
Task<string> Fetch([Url] string absoluteUrl, [Query] string token);
api.Fetch("https://cdn.example.com/data", "abc");
>>> GET https://cdn.example.com/data?token=abc
Shared route prefix with [PathPrefix]
When every method on an interface sits under the same route prefix, put a [PathPrefix] on the interface instead of
repeating it in each route. The prefix is prepended to every method's relative path with exactly one / between them,
before the base address is applied:
[PathPrefix("/api/v2")]
public interface IUsersApi
{
[Get("/users")] // -> /api/v2/users
Task<List<User>> GetAll();
[Get("/users/{id}")] // -> /api/v2/users/{id}
Task<User> Get(int id);
[Get("/search")] // -> /api/v2/search?query=...
Task<List<User>> Search(string query);
}
Slashes are normalized so you never get a double slash: a leading or trailing slash on the prefix, and a leading slash
on the route, are all tolerated ([PathPrefix("/api/v2/")] + [Get("users")] is still /api/v2/users). An empty or
whitespace prefix is a no-op, and existing {placeholder} substitution and query strings are preserved.
The prefix that applies is the one declared on the interface the client is generated for - the T in
RestService.For<T> or AddRefitClient<T>. It applies to every method the client exposes, including methods
inherited from base interfaces. Prefixes are not concatenated across interface inheritance; a base interface's own
[PathPrefix] applies only when that base interface is itself the client type:
[PathPrefix("/root")]
public interface IPingApi
{
[Get("/ping")]
Task<string> Ping();
}
[PathPrefix("/api/v2")]
public interface IUsersApi : IPingApi
{
[Get("/users")]
Task<List<User>> GetAll();
}
// RestService.For<IUsersApi>(...): Ping -> /api/v2/ping, GetAll -> /api/v2/users
// RestService.For<IPingApi>(...): Ping -> /root/ping
Querystrings
Dynamic Querystring Parameters
If you specify an object as a query parameter, all public properties which are not null are used as query parameters.
This previously only applied to GET requests, but has now been expanded to all HTTP request methods, partly thanks to
Twitter's hybrid API that insists on non-GET requests with querystring parameters.
Use the Query attribute to change the behavior to 'flatten' your query parameter object. If using this Attribute you
can specify values for the Delimiter and the Prefix which are used to 'flatten' the object.
public class MyQueryParams
{
[AliasAs("order")]
public string SortOrder { get; set; }
public int Limit { get; set; }
public KindOptions Kind { get; set; }
}
public enum KindOptions
{
Foo,
[EnumMember(Value = "bar")]
Bar
}
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, MyQueryParams params);
[Get("/group/{id}/users")]
Task<List<User>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".","search")] MyQueryParams params);
params.SortOrder = "desc";
params.Limit = 10;
params.Kind = KindOptions.Bar;
GroupList(4, params)
>>> "/group/4/users?order=desc&Limit=10&Kind=bar"
GroupListWithAttribute(4, params)
>>> "/group/4/users?search.order=desc&search.Limit=10&search.Kind=bar"
A similar behavior exists if using a Dictionary, but without the advantages of the AliasAs attributes and of course no
intellisense and/or type safety.
You can also specify querystring parameters with [Query] and have them flattened in non-GET requests, similar to:
[Post("/statuses/update.json")]
Task<Tweet> PostTweet([Query]TweetParams params);
Where TweetParams is a POCO, and properties will also support [AliasAs] attributes.
If you need to keep internal-only properties on your query DTO, mark them with one of the standard ignore attributes and Refit will skip them when building the query string:
[IgnoreDataMember][System.Text.Json.Serialization.JsonIgnore][Newtonsoft.Json.JsonIgnore]
Collections as Querystring parameters
Use the Query attribute to specify format in which collections should be formatted in query string
[Get("/users/list")]
Task Search([Query(CollectionFormat.Multi)]int[] ages);
Search(new [] {10, 20, 30})
>>> "/users/list?ages=10&ages=20&ages=30"
[Get("/users/list")]
Task Search([Query(CollectionFormat.Csv)]int[] ages);
Search(new [] {10, 20, 30})
>>> "/users/list?ages=10%2C20%2C30"
You can also specify collection format in RefitSettings, that will be used by default, unless explicitly defined in
Query attribute.
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
CollectionFormat = CollectionFormat.Multi
});
Indexed object collections (DeepObject / OpenAPI deepObject style)
Use CollectionFormat.Indexed to expand a collection of objects into indexed key–value pairs, where each element's
properties are flattened under the parameter name followed by the element index and the property name:
items[0].Id=1&items[0].Value=a&items[1].Id=2
This matches the OpenAPI 3 deepObject serialization style and is useful for APIs that expect an ordered list of
objects in the query string.
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
[Get("/orders")]
Task<Order> GetOrders([Query(CollectionFormat.Indexed)] IReadOnlyList<OrderItem> items);
GetOrders(new[] {
new OrderItem { ProductId = 1, Quantity = 2 },
new OrderItem { ProductId = 5, Quantity = 1 }
})
>>> "/orders?items[0].ProductId=1&items[0].Quantity=2&items[1].ProductId=5&items[1].Quantity=1"
Property names honor [AliasAs], [JsonPropertyName] (when RefitSettings.HonorContentSerializerPropertyNamesInQuery
is set) and [Query(Prefix)], exactly like a normal [Query] object parameter. Null elements in the collection are
skipped and the index counter still advances so the remaining indices remain stable.
Unescape Querystring parameters
Use the QueryUriFormat attribute to specify if the query parameters should be url escaped
[Get("/query")]
[QueryUriFormat(UriFormat.Unescaped)]
Task Query(string q);
Query("Select+Id,Name+From+Account")
>>> "/query?q=Select+Id,Name+From+Account"
For a single pre-encoded value, mark the parameter with [Encoded] (the equivalent of Retrofit's encoded = true)
and Refit passes it through verbatim while the rest of the request encodes normally. It applies to query values and
to path segments, including round-tripping {**param} segments — the caller becomes responsible for producing valid
encoded output:
[Get("/calendars/{calId}/events/{**eventId}")]
Task<CalendarEvent> GetEvent(string calId, [Encoded] string eventId);
GetEvent("work", "3bf0000488fda0ec154ee%40zoho.com")
>>> "/calendars/work/events/3bf0000488fda0ec154ee%40zoho.com"
[Encoded] (and [QueryName] below) are handled by generated request building only; using them on a method that
cannot generate inline is a compile-time error (RF007).
Valueless query flags
Some APIs use bare presence-style query switches with no value. Mark a parameter with [QueryName] (the equivalent
of Retrofit's @QueryName) and its value becomes the query segment itself; collections render one flag per element
and null values are omitted:
[Get("/items")]
Task<List<Item>> List([QueryName] string flag);
List("archived")
>>> "/items?archived"
[Get("/items")]
Task<List<Item>> List([QueryName] string[] flags);
List(["a", "b", "c"])
>>> "/items?a&b&c"
Custom Querystring parameter formatting
Formatting Keys
To customize the format of query keys, you have two main options:
Using the
AliasAsAttribute:You can use the
AliasAsattribute to specify a custom key name for a property. This attribute will always take precedence over any key formatter you specify.public class MyQueryParams { [AliasAs("order")] public string SortOrder { get; set; } public int Limit { get; set; } } [Get("/group/{id}/users")] Task<List<User>> GroupList([AliasAs("id")] int groupId, [Query] MyQueryParams params); params.SortOrder = "desc"; params.Limit = 10; GroupList(1, params);This will generate the following request:
/group/1/users?order=desc&Limit=10Using the
RefitSettings.UrlParameterKeyFormatterProperty:By default, Refit uses the property name as the query key without any additional formatting. If you want to apply a custom format across all your query keys, you can use the
UrlParameterKeyFormatterproperty. Remember that if a property has anAliasAsattribute, it will be used regardless of the formatter.The following example uses the built-in
CamelCaseUrlParameterKeyFormatter:public class MyQueryParams { public string SortOrder { get; set; } [AliasAs("queryLimit")] public int Limit { get; set; } } [Get("/group/users")] Task<List<User>> GroupList([Query] MyQueryParams params); params.SortOrder = "desc"; params.Limit = 10;The request will look like:
/group/users?sortOrder=desc&queryLimit=10
Note: The AliasAs attribute always takes the top priority. If both the attribute and a custom key formatter are
present, the AliasAs attribute's value will be used.
Built-in key formatters and naming-convention presets:
Refit ships CamelCaseUrlParameterKeyFormatter, SnakeCaseUrlParameterKeyFormatter, and
KebabCaseUrlParameterKeyFormatter. To apply a single naming convention consistently across query keys, form field
names and the JSON request body, use the RefitSettings presets — each wires up the matching
UrlParameterKeyFormatter and JsonNamingPolicy together:
var api = RestService.For<IMyApi>("https://api.example.com", RefitSettings.SnakeCase());
// also available: RefitSettings.KebabCase() and RefitSettings.CamelCase()
Per-property key prefix and delimiter:
A [Query] attribute on a property of a complex query object customizes that property's key as
{prefix}{delimiter}{name} (matching how form fields are named):
public class Form
{
[Query("-", "dontlog")]
public string Password { get; set; }
}
// => ?dontlog-Password=...
Serializing a value object via ToString():
By default a complex query parameter is flattened into its public properties. To instead send a single value using the
object's ToString() under the parameter's own name, mark it with an explicit empty format [Query(Format = "")] (or
[Query(TreatAsString = true)]):
[Get("/info")]
Task<string> GetInfo([Query(Format = "")] Size size); // => ?size=medium (uses size.ToString())
Custom query keys with IQueryConverter<T>:
When a parameter shape cannot be flattened from its declared type (an object, a polymorphic base type, a
Dictionary<string, object>), or you simply need full control over the emitted keys, implement IQueryConverter<T>
and attach it to the parameter with [QueryConverter(typeof(...))]. The converter writes query pairs straight into the
pooled builder, so it can emit nested bracket keys such as order[createdAt]=desc without [AliasAs("order[")]-style
hacks. This is a source-generator-only feature (the reflection request builder walks the value's runtime type instead);
implementations must be stateless and have a public parameterless constructor.
public sealed class SortOrderQueryConverter : IQueryConverter<IDictionary<string, string>>
{
public void Flatten(
IDictionary<string, string> value,
string keyPrefix, // the resolved [Query(Prefix)] for the parameter, or an empty string
ref GeneratedQueryStringBuilder builder,
RefitSettings settings)
{
foreach (var entry in value)
{
// AddPreEscapedKey appends the key verbatim, so the brackets stay literal while the value is
// still escaped. Use builder.Add(key, value, false) to percent-encode the key as well.
builder.AddPreEscapedKey($"{keyPrefix}order[{entry.Key}]", entry.Value, false);
}
}
}
[Get("/items")]
Task<List<Item>> GetItems(
[QueryConverter(typeof(SortOrderQueryConverter))] IDictionary<string, string> order);
GetItems(new Dictionary<string, string> { ["createdAt"] = "desc", ["priority"] = "asc" });
>>> "/items?order[createdAt]=desc&order[priority]=asc"
Formatting URL Parameter Values with the UrlParameterFormatter
In Refit, the UrlParameterFormatter property within RefitSettings allows you to customize how parameter values are
formatted in the URL. This can be particularly useful when you need to format dates, numbers, or other types in a
specific manner that aligns with your API's expectations.
Using UrlParameterFormatter:
Assign a custom formatter that implements the IUrlParameterFormatter interface to the UrlParameterFormatter
property.
public class CustomDateUrlParameterFormatter : IUrlParameterFormatter
{
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
{
if (value is DateTime dt)
{
return dt.ToString("yyyyMMdd");
}
return value?.ToString();
}
}
var settings = new RefitSettings
{
UrlParameterFormatter = new CustomDateUrlParameterFormatter()
};
In this example, a custom formatter is created for date values. Whenever a DateTime parameter is encountered, it
formats the date as yyyyMMdd.
Formatting Dictionary Keys:
When dealing with dictionaries, it's important to note that keys are treated as values. If you need custom formatting
for dictionary keys, you should use the UrlParameterFormatter as well.
For instance, if you have a dictionary parameter and you want to format its keys in a specific way, you can handle that in the custom formatter:
public class CustomDictionaryKeyFormatter : IUrlParameterFormatter
{
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
{
// Handle dictionary keys
if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
// Custom formatting logic for dictionary keys
return value?.ToString().ToUpperInvariant();
}
return value?.ToString();
}
}
var settings = new RefitSettings
{
UrlParameterFormatter = new CustomDictionaryKeyFormatter()
};
In the above example, the dictionary keys will be converted to uppercase.
Registering a formatter per type with UrlParameterFormatterMap:
To customize how one specific type is rendered into a URL without hand-rolling a type switch inside a custom
IUrlParameterFormatter, register a formatter for that type in RefitSettings.UrlParameterFormatterMap. When a value
is rendered into a path or query string, its runtime type is looked up in the map first; a registered formatter wins,
and every other type falls back to UrlParameterFormatter.
public class TemperatureUrlParameterFormatter : IUrlParameterFormatter
{
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type) =>
value is Temperature t ? $"{t.Celsius}deg" : value?.ToString();
}
var settings = new RefitSettings();
settings.UrlParameterFormatterMap[typeof(Temperature)] = new TemperatureUrlParameterFormatter();
Matching is by exact runtime type only — there is no base-class or interface walking, so register the concrete type the value will have at runtime. The registry applies to path parameters, round-trip path segments, and query values (it does not affect header or body serialization), and both the reflection and source-generated request builders consult it identically.
Body content
One of the parameters in your method can be used as the body, by using the Body attribute:
[Post("/users/new")]
Task CreateUser([Body] User user);
There are four possibilities for supplying the body data, depending on the type of the parameter:
- If the type is
Stream, the content will be streamed viaStreamContent - If the type is
string, the string will be used directly as the content unless[Body(BodySerializationMethod.Json)]is set which will send it as aStringContent - If the parameter has the attribute
[Body(BodySerializationMethod.UrlEncoded)], the content will be URL-encoded (see form posts below) - If the parameter has the attribute
[Body(BodySerializationMethod.JsonLines)], an enumerable body is sent as JSON Lines (see JSON Lines content below) - For all other types, the object will be serialized using the content serializer specified in RefitSettings (JSON is the default).
Buffering and the Content-Length header
By default, Refit streams the body content without buffering it. This means you can
stream a file from disk, for example, without incurring the overhead of loading
the whole file into memory. The downside of this is that no Content-Length header
is set on the request. If your API needs you to send a Content-Length header with
the request, you can disable this streaming behavior by setting the buffered argument
of the [Body] attribute to true:
Task CreateUser([Body(buffered: true)] User user);
JSON content
JSON requests and responses are serialized/deserialized using an instance of the IHttpContentSerializer interface.
Refit provides two implementations out of the box: SystemTextJsonContentSerializer (which is the default JSON
serializer) and NewtonsoftJsonContentSerializer. The first uses System.Text.Json APIs and is focused on high
performance and low memory usage, while the latter uses the known Newtonsoft.Json library and is more versatile and
customizable. You can read more about the two serializers and the main differences between the
two at this link.
The default System.Text.Json serializer uses camelCase property names, case-insensitive matching, and reads numbers
from JSON strings (NumberHandling = AllowReadingFromString). Override any of these by starting from Refit's defaults,
tweaking the JsonSerializerOptions, and passing them in:
var options = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
options.NumberHandling = JsonNumberHandling.Strict; // opt out of reading numbers from strings
var settings = new RefitSettings(new SystemTextJsonContentSerializer(options));
Fast-path source-generated serialization
The default options add custom converters and set NumberHandling, which the System.Text.Json source generator does
not support on its serialization fast-path,
so the default serializer always uses the (slower) metadata logic. If you want the fast-path, start from
GetFastPathJsonSerializerOptions() instead — it omits the converters and NumberHandling so the options stay
fast-path eligible — and assign a source-generated TypeInfoResolver: