GitHub

Refit

Build codecov

Refit Refit.HttpClientFactory Refit.Newtonsoft.Json Refit.Testing
NuGet NuGet NuGet NuGet 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

Refit is sponsored by the following:

lombiq logojetbrains logoclaude logo

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:

  • ContentSerializer
  • UrlParameterFormatter
  • UrlParameterKeyFormatter
  • CollectionFormat
  • AuthorizationHeaderValueGetter
  • ExceptionFactory
  • DeserializationExceptionFactory
  • TransportExceptionFactory
  • HttpRequestMessageOptions
  • Version and VersionPolicy

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] (including Format and CollectionFormat), 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 are StreamPart/ByteArrayPart/FileInfoPart (or a MultipartItem subclass), Stream, string, FileInfo, byte[], HttpContent, a date/time or Guid value, 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 wrappers
  • IAsyncEnumerable<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 CancellationToken parameter
  • [HeaderCollection] parameters that are not IDictionary<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 ArgumentException surfaces on the first call rather than from RestService.For<T>(...). Under UrlResolutionMode.Rfc3986 the 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 ArgumentException when 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 an ArgumentException.
  • [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:

  1. Using the AliasAs Attribute:

    You can use the AliasAs attribute 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=10
    
  2. Using the RefitSettings.UrlParameterKeyFormatter Property:

    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 UrlParameterKeyFormatter property. Remember that if a property has an AliasAs attribute, 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 via StreamContent
  • 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 a StringContent
  • 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:

var options = SystemTextJsonContentSerializer.GetFastPathJsonSerializerOptions();
options.TypeInfoResolver = MyJsonContext.Default; // a JsonSerializerContext you declare
var settings = new RefitSettings(new SystemTextJsonContentSerializer(options));
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(MyRequest))]
internal partial class MyJsonContext : JsonSerializerContext;

Two caveats: dropping the converters means object-typed members are no longer inferred and enums are no longer written as camelCase strings (add your own converters only if you accept losing the fast-path); and the fast-path runs through the synchronous serialization primitives (SerializeToUtf8Bytes, Serialize(Utf8JsonWriter, ...)) — the one API that bypasses it is the built-in JsonSerializer.SerializeAsync(Stream, ...).

By default Refit serializes request bodies with JsonContent, which uses exactly that SerializeAsync(Stream) path, so the fast-path is not used even with fast-path-eligible options. To opt in, set RequestBodySerialization on RefitSettings to one of the synchronous modes, which serialize through the fast-path:

  • RequestBodySerializationMode.Buffered — serialize synchronously to UTF-8 bytes up front and send as a ByteArrayContent. Sets a Content-Length header; best for small-to-medium bodies.
  • RequestBodySerializationMode.Streamed — serialize through a Utf8JsonWriter written to the request stream and flushed asynchronously. No Content-Length, but peak memory is bounded (pooled writer chunks rather than the whole body); best for large uploads.
var options = SystemTextJsonContentSerializer.GetFastPathJsonSerializerOptions();
options.TypeInfoResolver = MyJsonContext.Default;
var settings = new RefitSettings(new SystemTextJsonContentSerializer(options))
{
    RequestBodySerialization = RequestBodySerializationMode.Buffered, // or .Streamed for large uploads
};

Both modes require the content serializer to implement ISynchronousContentSerializer (the default SystemTextJsonContentSerializer does); otherwise the body falls back to the default asynchronous serialization.

Compressing the request body

Set Compression on [Body] to send the body under a content coding. Refit compresses whatever the serializer produced and sets Content-Encoding to match, so the coding composes with every serialization method:

public interface IUploadApi
{
    [Post("/measurements")]
    Task Upload([Body(Compression = RequestCompression.GZip)] Measurement[] batch);
    [Post("/measurements")]
    Task UploadSmallest([Body(
        Compression = RequestCompression.Brotli,
        CompressionLevel = CompressionLevel.SmallestSize)] Measurement[] batch);
}

To compress every body from one client, set it on the settings instead. A [Body] parameter naming its own coding overrides that, and RequestCompression.None on the parameter opts a single method out:

var settings = new RefitSettings
{
    RequestCompression = RequestCompression.GZip,
    RequestCompressionLevel = CompressionLevel.Optimal,
};

For knobs a level cannot express — window size, strategy, a Zstandard dictionary — set the compressor's own options. Options set for a coding replace the level for that coding; the codings left unset still compress by level:

var settings = new RefitSettings
{
    RequestCompression = RequestCompression.Brotli,
    RequestCompressionOptions = new()
    {
        Brotli = new() { Quality = 9, WindowLog2 = 22 },
        GZip = new() { CompressionLevel = 6 },
    },
};

RequestCompressionOptions needs .NET 9.0 or later, where ZLibCompressionOptions and BrotliCompressionOptions were introduced; its Zstandard property needs .NET 11.0.

There is no negotiation for a compressed request body, so only turn this on against a server you know accepts one. The compressed length is unknown until the body has been written, so these requests are sent chunked.

Codings are not available on every target framework, and asking for one the running framework cannot produce throws PlatformNotSupportedException when the request is built rather than quietly sending the body uncompressed:

Coding Content-Encoding Available on
RequestCompression.GZip gzip every target
RequestCompression.Brotli br .NET 8.0 and later
RequestCompression.Zstandard zstd .NET 11.0 and later

For instance, here is how to create a new RefitSettings instance using the Newtonsoft.Json-based serializer (you'll also need to add a PackageReference to Refit.Newtonsoft.Json):

var settings = new RefitSettings(new NewtonsoftJsonContentSerializer());

If you're using Newtonsoft.Json APIs, you can customize their behavior by setting the Newtonsoft.Json.JsonConvert.DefaultSettings property:

JsonConvert.DefaultSettings =
    () => new JsonSerializerSettings() {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Converters = {new StringEnumConverter()}
    };
// Serialized as: {"day":"Saturday"}
await PostSomeStuff(new { Day = DayOfWeek.Saturday });

As these are global settings they will affect your entire application. It might be beneficial to isolate the settings for calls to a particular API. When creating a Refit generated live interface, you may optionally pass a RefitSettings that will allow you to specify what serializer settings you would like. This allows you to have different serializer settings for separate APIs:

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        ContentSerializer = new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings {
                ContractResolver = new SnakeCasePropertyNamesContractResolver()
        }
    )});
var otherApi = RestService.For<IOtherApi>("https://api.example.com",
    new RefitSettings {
        ContentSerializer = new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
        }
    )});

Property serialization/deserialization can be customized using Json.NET's JsonProperty attribute:

public class Foo
{
    // Works like [AliasAs("b")] would in form posts (see below)
    [JsonProperty(PropertyName="b")]
    public string Bar { get; set; }
}

JSON source generator

To apply the benefits of the new JSON source generator for System.Text.Json added in .NET 6, you can use SystemTextJsonContentSerializer with a custom instance of RefitSettings and JsonSerializerOptions:

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        ContentSerializer = new SystemTextJsonContentSerializer(MyJsonSerializerContext.Default.Options)
    });

When using System.Text.Json polymorphism features such as [JsonDerivedType] / [JsonPolymorphic], Refit serializes request bodies using the declared Refit method parameter type rather than the boxed runtime object. This ensures type discriminators configured on the base contract are preserved in outgoing request payloads.

JSON Lines content

Some APIs accept a batch of records as JSON Lines (newline-delimited JSON), where each line is a self-contained JSON document. Mark an enumerable body parameter with [Body(BodySerializationMethod.JsonLines)] and Refit serializes each element with the configured content serializer, writing one document per line:

public interface IDocumentApi
{
    [Post("/collections/companies/documents/import")]
    Task ImportAsync([Body(BodySerializationMethod.JsonLines)] IEnumerable<Company> documents);
}

The request body is streamed (one serialized element per line, separated by \n) with a content type of application/x-ndjson. A non-enumerable value is sent as a single line. If you need a different content type (for example text/plain), set it with a static header or a dynamic header.

Streaming responses with IAsyncEnumerable<T>

Declare a method that returns IAsyncEnumerable<T> to consume a large response without buffering the whole body. Refit reads the response with HttpCompletionOption.ResponseHeadersRead and yields each element as it is deserialized:

public interface IDocumentApi
{
    [Get("/documents")]
    IAsyncEnumerable<Company> GetDocuments();
}
await foreach (var company in api.GetDocuments())
{
    // each item is yielded as soon as it is read off the wire
}

The frame format is auto-detected from the response content type: a content type of application/jsonl, application/x-ndjson, or application/x-jsonlines is read as JSON Lines (one value per line); text/event-stream is read as Server-Sent Events (see below); anything else is read as a single streamed JSON array. To observe cancellation, add a CancellationToken parameter, or use WithCancellation(token) on the enumerable.

Streaming requires the configured ContentSerializer to implement IStreamingContentSerializer. The default SystemTextJsonContentSerializer does; a serializer that does not will throw NotSupportedException when the sequence is enumerated.

Consuming Server-Sent Events

When the endpoint responds with text/event-stream, Refit parses each Server-Sent Events (SSE) data event and deserializes its payload to T, yielding one item per event as it arrives (the event type/id envelope is dropped):

public interface IChatApi
{
    [Get("/chat/stream")]
    IAsyncEnumerable<ChatToken> StreamAsync();
}
await foreach (var token in api.StreamAsync())
{
    // one ChatToken per `data:` event, streamed live
}

If you need the raw event envelope (event type or id), return Task<Stream> and feed the unbuffered stream to System.Net.ServerSentEvents.SseParser directly.

XML Content

XML requests and responses are serialized/deserialized using System.Xml.Serialization.XmlSerializer. By default, Refit will use JSON content serialization, to use XML content configure the ContentSerializer to use the XmlContentSerializer:

var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",
    new RefitSettings {
        ContentSerializer = new XmlContentSerializer()
    });

Property serialization/deserialization can be customized using attributes found in the System.Xml.Serialization namespace:

    public class Foo
    {
        [XmlElement(Namespace = "https://www.w3.org/XML")]
        public string Bar { get; set; }
    }

The System.Xml.Serialization.XmlSerializer provides many options for serializing, those options can be set by providing an XmlContentSerializerSettings to the XmlContentSerializer constructor:

var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",
    new RefitSettings {
        ContentSerializer = new XmlContentSerializer(
            new XmlContentSerializerSettings
            {
                XmlReaderWriterSettings = new XmlReaderWriterSettings()
                {
                    ReaderSettings = new XmlReaderSettings
                    {
                        IgnoreWhitespace = true
                    }
                }
            }
        )
    });

Form posts

For APIs that take form posts (i.e. serialized as application/x-www-form-urlencoded), initialize the Body attribute with BodySerializationMethod.UrlEncoded.

The parameter can be an IDictionary:

public interface IMeasurementProtocolApi
{
    [Post("/collect")]
    Task Collect([Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> data);
}
var data = new Dictionary<string, object> {
    {"v", 1},
    {"tid", "UA-1234-5"},
    {"cid", new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")},
    {"t", "event"},
};
// Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
await api.Collect(data);

Or you can just pass any object and all public, readable properties will be serialized as form fields in the request. This approach allows you to alias property names using [AliasAs("whatever")] which can help if the API has cryptic field names:

public interface IMeasurementProtocolApi
{
    [Post("/collect")]
    Task Collect([Body(BodySerializationMethod.UrlEncoded)] Measurement measurement);
}
public class Measurement
{
    // Properties can be read-only and [AliasAs] isn't required
    public int v { get { return 1; } }
    [AliasAs("tid")]
    public string WebPropertyId { get; set; }
    [AliasAs("cid")]
    public Guid ClientId { get; set; }
    [AliasAs("t")]
    public string Type { get; set; }
    public object IgnoreMe { private get; set; }
}
var measurement = new Measurement {
    WebPropertyId = "UA-1234-5",
    ClientId = new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"),
    Type = "event"
};
// Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
await api.Collect(measurement);

If you have a type that has [JsonProperty(PropertyName)] attributes setting property aliases, Refit will use those too ([AliasAs] will take precedence where you have both). This means that the following type will serialize as one=value1&two=value2:

public class SomeObject
{
    [JsonProperty(PropertyName = "one")]
    public string FirstProperty { get; set; }
    [JsonProperty(PropertyName = "notTwo")]
    [AliasAs("two")]
    public string SecondProperty { get; set; }
}

NOTE: This use of AliasAs applies to querystring parameters and form body posts, but not to response objects; for aliasing fields on response objects, you'll still need to use [JsonProperty("full-property-name")].

Sending null values

By default, a property whose value is null is omitted from the form body (and from object querystrings). To send an explicit empty value (key=) for a null property instead of omitting it, set SerializeNull on its [Query] attribute:

public class Measurement
{
    [Query(SerializeNull = true)]
    public string? Note { get; set; }
}
// With Note = null, serialized as: ...&Note=

Reflection-free form serialization

With generated request building on (the default), Refit flattens strongly-typed form bodies at compile time, so no reflection runs at request time. This covers a concrete class or struct serialized with the built-in SystemTextJsonContentSerializer. An object, an IDictionary, a collection, or a custom IHttpContentSerializer falls back to the reflection path, which produces identical output.

A body whose properties are all simple scalars (strings, numbers, enums, other IFormattable values) takes a faster straight-line path: the generator unrolls each field directly, with no descriptor array, getter delegates, or boxing. A body with a collection or complex property keeps the descriptor path. All three paths produce identical output.

Setting request headers

Static headers

You can set one or more static request headers for a request applying a Headers attribute to the method:

[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<User> GetUser(string user);

Static headers can also be added to every request in the API by applying the Headers attribute to the interface:

[Headers("User-Agent: Awesome Octocat App")]
public interface IGitHubApi
{
    [Get("/users/{user}")]
    Task<User> GetUser(string user);
    [Post("/users/new")]
    Task CreateUser([Body] User user);
}

Dynamic headers

If the content of the header needs to be set at runtime, you can add a header with a dynamic value to a request by applying a Header attribute to a parameter:

[Get("/users/{user}")]
Task<User> GetUser(string user, [Header("Authorization")] string authorization);
// Will add the header "Authorization: token OAUTH-TOKEN" to the request
var user = await GetUser("octocat", "token OAUTH-TOKEN");

Adding an Authorization header is such a common use case that you can add an access token to a request by applying an Authorize attribute to a parameter and optionally specifying the scheme:

[Get("/users/{user}")]
Task<User> GetUser(string user, [Authorize("Bearer")] string token);
// Will add the header "Authorization: Bearer OAUTH-TOKEN}" to the request
var user = await GetUser("octocat", "OAUTH-TOKEN");
//note: the scheme defaults to Bearer if none provided

If you need to set multiple headers at runtime, you can add a IDictionary<string, string> and apply a HeaderCollection attribute to the parameter and it will inject the headers into the request:

[Get("/users/{user}")]
Task<User> GetUser(string user, [HeaderCollection] IDictionary<string, string> headers);
var headers = new Dictionary<string, string> {{"Authorization","Bearer tokenGoesHere"}, {"X-Tenant-Id","123"}};
var user = await GetUser("octocat", headers);

Bearer Authentication

Most APIs need some sort of Authentication. The most common is OAuth Bearer authentication. A header is added to each request of the form: Authorization: Bearer <token>. Refit makes it easy to insert your logic to get the token however your app needs, so you don't have to pass a token into each method.

  1. Add [Headers("Authorization: Bearer")] to the interface or methods which need the token.
  2. Set AuthorizationHeaderValueGetter in the RefitSettings instance. Refit will call your delegate each time it needs to obtain the token, so it's a good idea for your mechanism to cache the token value for some period within the token lifetime.

AuthorizationHeaderValueGetter works whether you create clients with RestService.For<T>("https://...") or supply your own HttpClient via RestService.For<T>(httpClient, settings). If your API methods accept a CancellationToken, that token is propagated to the getter delegate.

If your getter returns null, an empty string, or whitespace, Refit omits the Authorization header for that request instead of sending a blank Authorization: <scheme> value. This lets a single client make both authenticated and anonymous calls: return a token when you have one, and return an empty string to skip auth for that request. Omitting the Authorization placeholder entirely (no [Headers("Authorization: Bearer")]) skips auth for the whole method.

Scoped (per-request) authorization tokens with dependency injection

When you register a client through Refit.HttpClientFactory, you can resolve the token from dependency injection with AddAuthorizationHeaderValueProvider:

services.AddRefitClient<IMyApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
    .AddAuthorizationHeaderValueProvider((serviceProvider, request, cancellationToken) =>
    {
        var tokenService = serviceProvider.GetRequiredService<IMyTokenService>();
        return new ValueTask<string>(tokenService.GetTokenForCurrentRequest());
    });

The delegate receives an IServiceProvider, the outgoing HttpRequestMessage, and a CancellationToken, and returns the token to place in the Authorization header (returning null/empty/whitespace skips auth for that request, exactly like AuthorizationHeaderValueGetter).

Caveat: IHttpClientFactory pools message handlers for their configured lifetime, so a scoped service captured directly by a handler would bleed across requests. To keep the provider correctly per-request, this extension creates a fresh DI scope for every request and resolves your delegate from that scope's IServiceProvider, disposing the scope when the request completes. True per-request isolation therefore relies on either per-request state you read from the request argument, or ambient AsyncLocal-based state (such as a host-registered IHttpContextAccessor) that flows into the fresh scope. No Microsoft.AspNetCore.* reference is required.

Reducing header boilerplate with DelegatingHandlers (Authorization headers worked example)

Although we make provisions for adding dynamic headers at runtime directly in Refit, most use-cases would likely benefit from registering a custom DelegatingHandler in order to inject the headers as part of the HttpClient middleware pipeline thus removing the need to add lots of [Header] or [HeaderCollection] attributes.

In the example above we are leveraging a [HeaderCollection] parameter to inject an Authorization and X-Tenant-Id header. This is quite a common scenario if you are integrating with a 3rd party that uses OAuth2. While it's ok for the occasional endpoint, it would be quite cumbersome if we had to add that boilerplate to every method in our interface.

In this example we will assume our application is a multi-tenant application that is able to pull information about a tenant through some interface ITenantProvider and has a data store IAuthTokenStore that can be used to retrieve an auth token to attach to the outbound request.

 //Custom delegating handler for adding Auth headers to outbound requests
 class AuthHeaderHandler : DelegatingHandler
 {
     private readonly ITenantProvider tenantProvider;
     private readonly IAuthTokenStore authTokenStore;
    public AuthHeaderHandler(ITenantProvider tenantProvider, IAuthTokenStore authTokenStore)
    {
         this.tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider));
         this.authTokenStore = authTokenStore ?? throw new ArgumentNullException(nameof(authTokenStore));
         // InnerHandler must be left as null when using DI, but must be assigned a value when
         // using RestService.For<IMyApi>
         // InnerHandler = new HttpClientHandler();
    }
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await authTokenStore.GetToken();
        //potentially refresh token here if it has expired etc.
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        request.Headers.Add("X-Tenant-Id", tenantProvider.GetTenantId());
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ITenantProvider, TenantProvider>();
    services.AddTransient<IAuthTokenStore, AuthTokenStore>();
    services.AddTransient<AuthHeaderHandler>();
    //this will add our refit api implementation with an HttpClient
    //that is configured to add auth headers to all requests
    //note: AddRefitClient<T> requires a reference to Refit.HttpClientFactory
    //note: the order of delegating handlers is important and they run in the order they are added!
    services.AddRefitClient<ISomeThirdPartyApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
        .AddHttpMessageHandler<AuthHeaderHandler>();
        //you could add Polly here to handle HTTP 429 / HTTP 503 etc
}
//Your application code
public class SomeImportantBusinessLogic
{
    private ISomeThirdPartyApi thirdPartyApi;
    public SomeImportantBusinessLogic(ISomeThirdPartyApi thirdPartyApi)
    {
        this.thirdPartyApi = thirdPartyApi;
    }
    public async Task DoStuffWithUser(string username)
    {
        var user = await thirdPartyApi.GetUser(username);
        //do your thing
    }
}

If you aren't using dependency injection then you could achieve the same thing by doing something like this:

var api = RestService.For<ISomeThirdPartyApi>(new HttpClient(new AuthHeaderHandler(tenantProvider, authTokenStore))
    {
        BaseAddress = new Uri("https://api.example.com")
    }
);
var user = await thirdPartyApi.GetUser(username);
//do your thing

Redefining headers

Unlike Retrofit, where headers do not overwrite each other and are all added to the request regardless of how many times the same header is defined, Refit takes a similar approach to the approach ASP.NET MVC takes with action filters — redefining a header will replace it, in the following order of precedence:

  • Headers attribute on the interface (lowest priority)
  • Headers attribute on the method
  • Header attribute or HeaderCollection attribute on a method parameter (highest priority)
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
    [Get("/users/list")]
    Task<List> GetUsers();
    [Get("/users/{user}")]
    [Headers("X-Emoji: :smile_cat:")]
    Task<User> GetUser(string user);
    [Post("/users/new")]
    [Headers("X-Emoji: :metal:")]
    Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji);
}
// X-Emoji: :rocket:
var users = await GetUsers();
// X-Emoji: :smile_cat:
var user = await GetUser("octocat");
// X-Emoji: :trollface:
await CreateUser(user, ":trollface:");

Note: This redefining behavior only applies to headers with the same name. Headers with different names are not replaced. The following code will result in all headers being included:

[Headers("Header-A: 1")]
public interface ISomeApi
{
    [Headers("Header-B: 2")]
    [Post("/post")]
    Task PostTheThing([Header("Header-C")] int c);
}
// Header-A: 1
// Header-B: 2
// Header-C: 3
var user = await api.PostTheThing(3);

Removing headers

Headers defined on an interface or method can be removed by redefining a static header without a value (i.e. without : <value>) or passing null for a dynamic header. Empty strings will be included as empty headers.

[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
    [Get("/users/list")]
    [Headers("X-Emoji")] // Remove the X-Emoji header
    Task<List> GetUsers();
    [Get("/users/{user}")]
    [Headers("X-Emoji:")] // Redefine the X-Emoji header as empty
    Task<User> GetUser(string user);
    [Post("/users/new")]
    Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji);
}
// No X-Emoji header
var users = await GetUsers();
// X-Emoji:
var user = await GetUser("octocat");
// No X-Emoji header
await CreateUser(user, null);
// X-Emoji:
await CreateUser(user, "");

Validating header values

By default Refit adds header values verbatim with HttpHeaders.TryAddWithoutValidation, so values are sent exactly as supplied. This is deliberate: many valid header values (custom User-Agent strings, comma-separated list headers, and some date formats) are rejected by the framework's strict header parsers even though servers accept them.

If you would rather have malformed header values fail fast, set RefitSettings.ValidateHeaders to true. Refit then applies headers with HttpHeaders.Add, which validates each value against its header parser and throws a FormatException while the request is being built if a value is malformed:

var settings = new RefitSettings
{
    ValidateHeaders = true // default is false (values sent verbatim)
};
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com", settings);

The default (false) preserves Refit's long-standing behaviour. Carriage-return and line-feed characters are stripped from header names and values in both modes to guard against header injection, so turning this on only changes whether otherwise-malformed values are surfaced as an exception rather than sent as-is.

Passing state into DelegatingHandlers

If there is runtime state that you need to pass to a DelegatingHandler you can add a property with a dynamic value to the underlying HttpRequestMessage.Properties by applying a Property attribute to a parameter:

public interface IGitHubApi
{
    [Post("/users/new")]
    Task CreateUser([Body] User user, [Property("SomeKey")] string someValue);
    [Post("/users/new")]
    Task CreateUser([Body] User user, [Property] string someOtherKey);
}

The attribute constructor optionally takes a string which becomes the key in the HttpRequestMessage.Properties dictionary. If no key is explicitly defined then the name of the parameter becomes the key. If a key is defined multiple times the value in HttpRequestMessage.Properties will be overwritten. The parameter itself can be any object. Properties can be accessed inside a DelegatingHandler as follows:

⚠️ Important for IHttpClientFactory users: DelegatingHandler instances are pooled and can live longer than a single request scope. Avoid reading per-request state from services that may be scoped/cached across handler lifetimes ( for example a tenant/customer resolver stored on the handler). For per-request values like CustomerId, pass the value through [Property] so each request carries its own state.

class RequestPropertyHandler : DelegatingHandler
{
    public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // See if the request has a the property
        if(request.Properties.ContainsKey("SomeKey"))
        {
            var someProperty = request.Properties["SomeKey"];
            //do stuff
        }
        if(request.Properties.ContainsKey("someOtherKey"))
        {
            var someOtherProperty = request.Properties["someOtherKey"];
            //do stuff
        }
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

Note: in .NET 5 HttpRequestMessage.Properties has been marked Obsolete and Refit will instead populate the value into the new HttpRequestMessage.Options.

Support for Polly and Polly.Context

Because Refit supports HttpClientFactory it is possible to configure Polly policies on your HttpClient. If your policy makes use of Polly.Context this can be passed via Refit by adding [Property("PolicyExecutionContext")] Polly.Context context as behind the scenes Polly.Context is simply stored in HttpRequestMessage.Properties under the key PolicyExecutionContext and is of type Polly.Context. It's only recommended to pass the Polly.Context this way if your use case requires that the Polly.Context be initialized with dynamic content only known at runtime. If your Polly.Context only requires the same content every time (e.g an ILogger that you want to use to log from inside your policies) a cleaner approach is to inject the Polly.Context via a DelegatingHandler as described in #801

Target Interface Type and method info

There may be times when you want to know what the target interface type is of the Refit instance. An example is where you have a derived interface that implements a common base like this:

public interface IGetAPI<TEntity>
{
    [Get("/{key}")]
    Task<TEntity> Get(long key);
}
public interface IUsersAPI : IGetAPI<User>
{
}
public interface IOrdersAPI : IGetAPI<Order>
{
}

You can access the concrete type of the interface for use in a handler, such as to alter the URL of the request:

class RequestPropertyHandler : DelegatingHandler
{
    public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Get the type of the target interface
        Type interfaceType = (Type)request.Properties[HttpMessageRequestOptions.InterfaceType];
        var builder = new UriBuilder(request.RequestUri);
        // Alter the Path in some way based on the interface or an attribute on it
        builder.Path = $"/{interfaceType.Name}{builder.Path}";
        // Set the new Uri on the outgoing message
        request.RequestUri = builder.Uri;
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

The full method information (RestMethodInfo) is also always available in the request options. The RestMethodInfo contains more information about the method being called such as the full MethodInfo when using reflection is needed:

class RequestPropertyHandler : DelegatingHandler
{
    public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Get the method info
        if (request.Options.TryGetValue(new HttpRequestOptionsKey<RestMethodInfo>(HttpRequestMessageOptions.RestMethodInfo), out RestMethodInfo restMethodInfo))
        {
            var builder = new UriBuilder(request.RequestUri);
            // Alter the Path in some way based on the method info or an attribute on it
            builder.Path = $"/{restMethodInfo.MethodInfo.Name}{builder.Path}";
            // Set the new Uri on the outgoing message
            request.RequestUri = builder.Uri;
        }
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

Note: in .NET 5 HttpRequestMessage.Properties has been marked Obsolete and Refit will instead populate the value into the new HttpRequestMessage.Options. Refit provides HttpRequestMessageOptions.InterfaceType and HttpRequestMessageOptions.RestMethodInfo to respectively access the interface type and REST method info from the options.

Every request also carries two lightweight string options that a handler can read without touching reflection: HttpRequestMessageOptions.MethodName (the interface method's name, for example GetUser) and HttpRequestMessageOptions.RelativePathTemplate (the raw route template with its {placeholders}, for example /users/{id}). Both are populated identically whether the request was built by the source generator or by the reflection request builder. RelativePathTemplate is the stable, low-cardinality label to use for OpenTelemetry spans, metrics, and logs - unlike the filled RequestUri (/users/123), one template value groups every call to the same endpoint instead of exploding cardinality per id.

class TelemetryHandler : DelegatingHandler
{
    public TelemetryHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Options.TryGetValue(new HttpRequestOptionsKey<string>(HttpRequestMessageOptions.MethodName), out var methodName) &&
            request.Options.TryGetValue(new HttpRequestOptionsKey<string>(HttpRequestMessageOptions.RelativePathTemplate), out var routeTemplate))
        {
            // routeTemplate is "/users/{id}" (low cardinality), not the filled "/users/123"
            using var activity = ActivitySource.StartActivity($"{methodName} {routeTemplate}");
        }
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

Inspecting the current call's arguments

Set RefitSettings.CaptureMethodArguments = true to expose the current call's argument values to a DelegatingHandler via HttpRequestMessageOptions.MethodArguments. The stored value is an object?[] holding the boxed arguments in the method's declared parameter order, including any CancellationToken, so it lines up 1:1 with the reflected parameter list. This mirrors Retrofit's Invocation.arguments.

"var settings = new RefitSettings { CaptureMethodArguments = true }; class ArgumentLoggingHandler : DelegatingHandler { public ArgumentLoggingHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {} protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Options.TryGetValue(new HttpRequestOptionsKey

Read the original on github.com ↗