| 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:
publicinterfaceIGitHubApi{[Get("/users/{user}")]Task<User>GetUser(stringuser);}The RestService class generates an implementation of IGitHubApi that uses
HttpClient to make its calls:
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com");varoctocat=awaitgitHubApi.GetUser("octocat");.NET supports registering Refit clients via HttpClientFactory:
services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c =>c.BaseAddress=newUri("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.
- Sponsors
- Where does this work?
- 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
- Testing your Refit clients
Refit is sponsored by the following:
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
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.
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:
varapi=RestService.For<IGitHubApi>("https://api.github.com");or through Refit.HttpClientFactory:
services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c =>c.BaseAddress=newUri("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
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.
For Native AoT or trimmed apps, RestService.ForGenerated<T> creates a client only when the generator registered an
implementation for that interface:
usingvarclient=newHttpClient{BaseAddress=newUri("https://api.github.com")};varapi=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.
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.
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>.
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")]intgroupId);A request url can also bind replacement blocks to a custom object
[Get("/group/{request.groupId}/users/{request.userId}")]Task<List<User>>GroupList(UserGroupRequestrequest);classUserGroupRequest{intgroupId{get;set;}intuserId{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>(Trequest)whereT: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(intgroupId,[AliasAs("sort")]stringsortOrder);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(stringpage);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(stringdeviceId,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 404Optional 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.
varsettings=newRefitSettings{AllowUnmatchedRouteParameters=true};varapi=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.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:
varsettings=newRefitSettings{UrlResolution=UrlResolutionMode.Rfc3986};varapi=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.
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.
publicinterfaceIFileApi{[Get("")]Task<Stream>Download([Url]stringabsoluteUrl);}// 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]stringabsoluteUrl,[Query]stringtoken);api.Fetch("https://cdn.example.com/data","abc");>>>GET https://cdn.example.com/data?token=abcWhen 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")]publicinterfaceIUsersApi{[Get("/users")]// -> /api/v2/usersTask<List<User>>GetAll();[Get("/users/{id}")]// -> /api/v2/users/{id}Task<User>Get(intid);[Get("/search")]// -> /api/v2/search?query=...Task<List<User>>Search(stringquery);}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")]publicinterfaceIPingApi{[Get("/ping")]Task<string>Ping();}[PathPrefix("/api/v2")]publicinterfaceIUsersApi:IPingApi{[Get("/users")]Task<List<User>>GetAll();}// RestService.For<IUsersApi>(...): Ping -> /api/v2/ping, GetAll -> /api/v2/users// RestService.For<IPingApi>(...): Ping -> /root/pingIf 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.
publicclassMyQueryParams{[AliasAs("order")]publicstringSortOrder{get;set;}publicintLimit{get;set;}publicKindOptionsKind{get;set;}}publicenumKindOptions{Foo,[EnumMember(Value="bar")]Bar}[Get("/group/{id}/users")]Task<List<User>>GroupList([AliasAs("id")]intgroupId,MyQueryParamsparams);[Get("/group/{id}/users")]Task<List<User>>GroupListWithAttribute([AliasAs("id")]intgroupId,[Query(".","search")]MyQueryParamsparams);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]TweetParamsparams);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]
Use the Query attribute to specify format in which collections should be formatted in query string
[Get("/users/list")]TaskSearch([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.
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{CollectionFormat=CollectionFormat.Multi});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.
publicclassOrderItem{publicintProductId{get;set;}publicintQuantity{get;set;}}[Get("/orders")]Task<Order>GetOrders([Query(CollectionFormat.Indexed)]IReadOnlyList<OrderItem>items);GetOrders(new[]{newOrderItem{ProductId=1,Quantity=2},newOrderItem{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.
Use the QueryUriFormat attribute to specify if the query parameters should be url escaped
[Get("/query")][QueryUriFormat(UriFormat.Unescaped)]TaskQuery(stringq);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(stringcalId,[Encoded]stringeventId);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).
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]stringflag);List("archived")>>>"/items?archived"[Get("/items")]
Task<List<Item>>List([QueryName]string[]flags);
List(["a","b","c"])>>>"/items?a&b&c"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.publicclassMyQueryParams{[AliasAs("order")]publicstringSortOrder{get;set;}publicintLimit{get;set;}}[Get("/group/{id}/users")]Task<List<User>>GroupList([AliasAs("id")]intgroupId,[Query]MyQueryParamsparams);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:publicclassMyQueryParams{publicstringSortOrder{get;set;}[AliasAs("queryLimit")]publicintLimit{get;set;}}[Get("/group/users")]Task<List<User>>GroupList([Query]MyQueryParamsparams);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:
varapi=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):
publicclassForm{[Query("-","dontlog")]publicstringPassword{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="")]Sizesize);// => ?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.
publicsealedclassSortOrderQueryConverter:IQueryConverter<IDictionary<string,string>>{publicvoidFlatten(IDictionary<string,string>value,stringkeyPrefix,// the resolved [Query(Prefix)] for the parameter, or an empty stringrefGeneratedQueryStringBuilderbuilder,RefitSettingssettings){foreach(varentryinvalue){// 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(newDictionary<string,string>{["createdAt"]="desc",["priority"]="asc"});>>>"/items?order[createdAt]=desc&order[priority]=asc"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.
publicclassCustomDateUrlParameterFormatter:IUrlParameterFormatter{publicstring?Format(object?value,ICustomAttributeProviderattributeProvider,Typetype){if(valueisDateTimedt){returndt.ToString("yyyyMMdd");}returnvalue?.ToString();}}varsettings=newRefitSettings{UrlParameterFormatter=newCustomDateUrlParameterFormatter()};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:
publicclassCustomDictionaryKeyFormatter:IUrlParameterFormatter{publicstring?Format(object?value,ICustomAttributeProviderattributeProvider,Typetype){// Handle dictionary keysif(attributeProviderisPropertyInfoprop&&prop.PropertyType.IsGenericType&&prop.PropertyType.GetGenericTypeDefinition()==typeof(Dictionary<,>)){// Custom formatting logic for dictionary keysreturnvalue?.ToString().ToUpperInvariant();}returnvalue?.ToString();}}varsettings=newRefitSettings{UrlParameterFormatter=newCustomDictionaryKeyFormatter()};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.
publicclassTemperatureUrlParameterFormatter:IUrlParameterFormatter{publicstring?Format(object?value,ICustomAttributeProviderattributeProvider,Typetype)=>valueisTemperaturet?$"{t.Celsius}deg":value?.ToString();}varsettings=newRefitSettings();settings.UrlParameterFormatterMap[typeof(Temperature)]=newTemperatureUrlParameterFormatter();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.
One of the parameters in your method can be used as the body, by using the Body attribute:
[Post("/users/new")]TaskCreateUser([Body]Useruser);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).
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:
TaskCreateUser([Body(buffered:true)]Useruser);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:
varoptions=SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();options.NumberHandling=JsonNumberHandling.Strict;// opt out of reading numbers from stringsvarsettings=newRefitSettings(newSystemTextJsonContentSerializer(options));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:
varoptions=SystemTextJsonContentSerializer.GetFastPathJsonSerializerOptions();options.TypeInfoResolver=MyJsonContext.Default;// a JsonSerializerContext you declarevarsettings=newRefitSettings(newSystemTextJsonContentSerializer(options));[JsonSourceGenerationOptions(PropertyNamingPolicy=JsonKnownNamingPolicy.CamelCase)][JsonSerializable(typeof(MyRequest))]internalpartialclassMyJsonContext: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 aByteArrayContent. Sets aContent-Lengthheader; best for small-to-medium bodies.RequestBodySerializationMode.Streamed— serialize through aUtf8JsonWriterwritten to the request stream and flushed asynchronously. NoContent-Length, but peak memory is bounded (pooled writer chunks rather than the whole body); best for large uploads.
varoptions=SystemTextJsonContentSerializer.GetFastPathJsonSerializerOptions();options.TypeInfoResolver=MyJsonContext.Default;varsettings=newRefitSettings(newSystemTextJsonContentSerializer(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.
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):
varsettings=newRefitSettings(newNewtonsoftJsonContentSerializer());If you're using Newtonsoft.Json APIs, you can customize their behavior by setting the
Newtonsoft.Json.JsonConvert.DefaultSettings property:
JsonConvert.DefaultSettings=()=>newJsonSerializerSettings(){ContractResolver=newCamelCasePropertyNamesContractResolver(),Converters={newStringEnumConverter()}};// Serialized as: {"day":"Saturday"}awaitPostSomeStuff(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:
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{ContentSerializer=newNewtonsoftJsonContentSerializer(newJsonSerializerSettings{ContractResolver=newSnakeCasePropertyNamesContractResolver()})});varotherApi=RestService.For<IOtherApi>("https://api.example.com",newRefitSettings{ContentSerializer=newNewtonsoftJsonContentSerializer(newJsonSerializerSettings{ContractResolver=newCamelCasePropertyNamesContractResolver()})});Property serialization/deserialization can be customized using Json.NET's JsonProperty attribute:
publicclassFoo{// Works like [AliasAs("b")] would in form posts (see below)[JsonProperty(PropertyName="b")]publicstringBar{get;set;}}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:
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{ContentSerializer=newSystemTextJsonContentSerializer(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.
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:
publicinterfaceIDocumentApi{[Post("/collections/companies/documents/import")]TaskImportAsync([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.
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:
publicinterfaceIDocumentApi{[Get("/documents")]IAsyncEnumerable<Company>GetDocuments();}awaitforeach(varcompanyinapi.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.
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):
publicinterfaceIChatApi{[Get("/chat/stream")]IAsyncEnumerable<ChatToken>StreamAsync();}awaitforeach(vartokeninapi.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 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:
vargitHubApi=RestService.For<IXmlApi>("https://www.w3.org/XML",newRefitSettings{ContentSerializer=newXmlContentSerializer()});Property serialization/deserialization can be customized using attributes found in the System.Xml.Serialization namespace:
publicclassFoo{[XmlElement(Namespace="https://www.w3.org/XML")]publicstringBar{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:
vargitHubApi=RestService.For<IXmlApi>("https://www.w3.org/XML",newRefitSettings{ContentSerializer=newXmlContentSerializer(newXmlContentSerializerSettings{XmlReaderWriterSettings=newXmlReaderWriterSettings(){ReaderSettings=newXmlReaderSettings{IgnoreWhitespace=true}}})});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:
publicinterfaceIMeasurementProtocolApi{[Post("/collect")]TaskCollect([Body(BodySerializationMethod.UrlEncoded)]Dictionary<string,object>data);}vardata=newDictionary<string,object>{{"v",1},{"tid","UA-1234-5"},{"cid",newGuid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")},{"t","event"},};// Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=eventawaitapi.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:
publicinterfaceIMeasurementProtocolApi{[Post("/collect")]TaskCollect([Body(BodySerializationMethod.UrlEncoded)]Measurementmeasurement);}publicclassMeasurement{// Properties can be read-only and [AliasAs] isn't requiredpublicintv{get{return1;}}[AliasAs("tid")]publicstringWebPropertyId{get;set;}[AliasAs("cid")]publicGuidClientId{get;set;}[AliasAs("t")]publicstringType{get;set;}publicobjectIgnoreMe{privateget;set;}}varmeasurement=newMeasurement{WebPropertyId="UA-1234-5",ClientId=newGuid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"),Type="event"};// Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=eventawaitapi.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:
publicclassSomeObject{[JsonProperty(PropertyName="one")]publicstringFirstProperty{get;set;}[JsonProperty(PropertyName="notTwo")][AliasAs("two")]publicstringSecondProperty{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")].
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:
publicclassMeasurement{[Query(SerializeNull=true)]publicstring?Note{get;set;}}// With Note = null, serialized as: ...&Note=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.
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(stringuser);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")]publicinterfaceIGitHubApi{[Get("/users/{user}")]Task<User>GetUser(stringuser);[Post("/users/new")]TaskCreateUser([Body]Useruser);}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(stringuser,[Header("Authorization")]stringauthorization);// Will add the header "Authorization: token OAUTH-TOKEN" to the requestvaruser=awaitGetUser("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(stringuser,[Authorize("Bearer")]stringtoken);// Will add the header "Authorization: Bearer OAUTH-TOKEN}" to the requestvaruser=awaitGetUser("octocat","OAUTH-TOKEN");//note: the scheme defaults to Bearer if none providedIf 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(stringuser,[HeaderCollection]IDictionary<string,string>headers);varheaders=newDictionary<string,string>{{"Authorization","Bearer tokenGoesHere"},{"X-Tenant-Id","123"}};varuser=awaitGetUser("octocat",headers);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.
- Add
[Headers("Authorization: Bearer")]to the interface or methods which need the token. - Set
AuthorizationHeaderValueGetterin theRefitSettingsinstance. 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.
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=newUri("https://api.example.com")).AddAuthorizationHeaderValueProvider((serviceProvider,request,cancellationToken)=>{vartokenService=serviceProvider.GetRequiredService<IMyTokenService>();returnnewValueTask<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.
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 requestsclassAuthHeaderHandler:DelegatingHandler{privatereadonlyITenantProvidertenantProvider;privatereadonlyIAuthTokenStoreauthTokenStore;publicAuthHeaderHandler(ITenantProvidertenantProvider,IAuthTokenStoreauthTokenStore){this.tenantProvider=tenantProvider??thrownewArgumentNullException(nameof(tenantProvider));this.authTokenStore=authTokenStore??thrownewArgumentNullException(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();}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){vartoken=awaitauthTokenStore.GetToken();//potentially refresh token here if it has expired etc.request.Headers.Authorization=newAuthenticationHeaderValue("Bearer",token);request.Headers.Add("X-Tenant-Id",tenantProvider.GetTenantId());returnawaitbase.SendAsync(request,cancellationToken).ConfigureAwait(false);}}//Startup.cspublicvoidConfigureServices(IServiceCollectionservices){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=newUri("https://api.example.com")).AddHttpMessageHandler<AuthHeaderHandler>();//you could add Polly here to handle HTTP 429 / HTTP 503 etc}//Your application codepublicclassSomeImportantBusinessLogic{privateISomeThirdPartyApithirdPartyApi;publicSomeImportantBusinessLogic(ISomeThirdPartyApithirdPartyApi){this.thirdPartyApi=thirdPartyApi;}publicasyncTaskDoStuffWithUser(stringusername){varuser=awaitthirdPartyApi.GetUser(username);//do your thing}}If you aren't using dependency injection then you could achieve the same thing by doing something like this:
varapi=RestService.For<ISomeThirdPartyApi>(newHttpClient(newAuthHeaderHandler(tenantProvider,authTokenStore)){BaseAddress=newUri("https://api.example.com")});varuser=awaitthirdPartyApi.GetUser(username);//do your thingUnlike 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:
Headersattribute on the interface (lowest priority)Headersattribute on the methodHeaderattribute orHeaderCollectionattribute on a method parameter (highest priority)
[Headers("X-Emoji: :rocket:")]publicinterfaceIGitHubApi{[Get("/users/list")]Task<List>GetUsers();[Get("/users/{user}")][Headers("X-Emoji: :smile_cat:")]Task<User>GetUser(stringuser);[Post("/users/new")][Headers("X-Emoji: :metal:")]TaskCreateUser([Body]Useruser,[Header("X-Emoji")]stringemoji);}// X-Emoji: :rocket:varusers=awaitGetUsers();// X-Emoji: :smile_cat:varuser=awaitGetUser("octocat");// X-Emoji: :trollface:awaitCreateUser(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")]publicinterfaceISomeApi{[Headers("Header-B: 2")][Post("/post")]TaskPostTheThing([Header("Header-C")]intc);}// Header-A: 1// Header-B: 2// Header-C: 3varuser=awaitapi.PostTheThing(3);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:")]publicinterfaceIGitHubApi{[Get("/users/list")][Headers("X-Emoji")]// Remove the X-Emoji headerTask<List>GetUsers();[Get("/users/{user}")][Headers("X-Emoji:")]// Redefine the X-Emoji header as emptyTask<User>GetUser(stringuser);[Post("/users/new")]TaskCreateUser([Body]Useruser,[Header("X-Emoji")]stringemoji);}// No X-Emoji headervarusers=awaitGetUsers();// X-Emoji:varuser=awaitGetUser("octocat");// No X-Emoji headerawaitCreateUser(user,null);// X-Emoji:awaitCreateUser(user,"");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:
varsettings=newRefitSettings{ValidateHeaders=true// default is false (values sent verbatim)};vargitHubApi=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.
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:
publicinterfaceIGitHubApi{[Post("/users/new")]TaskCreateUser([Body]Useruser,[Property("SomeKey")]stringsomeValue);[Post("/users/new")]TaskCreateUser([Body]Useruser,[Property]stringsomeOtherKey);}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 forIHttpClientFactoryusers:DelegatingHandlerinstances 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 likeCustomerId, pass the value through[Property]so each request carries its own state.
classRequestPropertyHandler:DelegatingHandler{publicRequestPropertyHandler(HttpMessageHandlerinnerHandler=null):base(innerHandler??newHttpClientHandler()){}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){// See if the request has a the propertyif(request.Properties.ContainsKey("SomeKey")){varsomeProperty=request.Properties["SomeKey"];//do stuff}if(request.Properties.ContainsKey("someOtherKey")){varsomeOtherProperty=request.Properties["someOtherKey"];//do stuff}returnawaitbase.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.
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
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:
publicinterfaceIGetAPI<TEntity>{[Get("/{key}")]Task<TEntity>Get(longkey);}publicinterfaceIUsersAPI:IGetAPI<User>{}publicinterfaceIOrdersAPI: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:
classRequestPropertyHandler:DelegatingHandler{publicRequestPropertyHandler(HttpMessageHandlerinnerHandler=null):base(innerHandler??newHttpClientHandler()){}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){// Get the type of the target interfaceTypeinterfaceType=(Type)request.Properties[HttpMessageRequestOptions.InterfaceType];varbuilder=newUriBuilder(request.RequestUri);// Alter the Path in some way based on the interface or an attribute on itbuilder.Path=$"/{interfaceType.Name}{builder.Path}";// Set the new Uri on the outgoing messagerequest.RequestUri=builder.Uri;returnawaitbase.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:
classRequestPropertyHandler:DelegatingHandler{publicRequestPropertyHandler(HttpMessageHandlerinnerHandler=null):base(innerHandler??newHttpClientHandler()){}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){// Get the method infoif(request.Options.TryGetValue(newHttpRequestOptionsKey<RestMethodInfo>(HttpRequestMessageOptions.RestMethodInfo),outRestMethodInforestMethodInfo)){varbuilder=newUriBuilder(request.RequestUri);// Alter the Path in some way based on the method info or an attribute on itbuilder.Path=$"/{restMethodInfo.MethodInfo.Name}{builder.Path}";// Set the new Uri on the outgoing messagerequest.RequestUri=builder.Uri;}returnawaitbase.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.
classTelemetryHandler:DelegatingHandler{publicTelemetryHandler(HttpMessageHandlerinnerHandler=null):base(innerHandler??newHttpClientHandler()){}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){if(request.Options.TryGetValue(newHttpRequestOptionsKey<string>(HttpRequestMessageOptions.MethodName),outvarmethodName)&&request.Options.TryGetValue(newHttpRequestOptionsKey<string>(HttpRequestMessageOptions.RelativePathTemplate),outvarrouteTemplate)){// routeTemplate is "/users/{id}" (low cardinality), not the filled "/users/123"usingvaractivity=ActivitySource.StartActivity($"{methodName}{routeTemplate}");}returnawaitbase.SendAsync(request,cancellationToken).ConfigureAwait(false);}}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.
varsettings=newRefitSettings{CaptureMethodArguments=true};classArgumentLoggingHandler:DelegatingHandler{publicArgumentLoggingHandler(HttpMessageHandlerinnerHandler=null):base(innerHandler??newHttpClientHandler()){}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){if(request.Options.TryGetValue(newHttpRequestOptionsKey<object?[]>(HttpRequestMessageOptions.MethodArguments),outvararguments)){// Reflection path: pair the values with RestMethodInfo.MethodInfo.GetParameters() to recover parameter names.if(request.Options.TryGetValue(newHttpRequestOptionsKey<RestMethodInfo>(HttpRequestMessageOptions.RestMethodInfo),outvarrestMethodInfo)){varparameters=restMethodInfo.MethodInfo.GetParameters();for(vari=0;i<arguments.Length;i++){Console.WriteLine($"{parameters[i].Name} = {arguments[i]}");}}}returnawaitbase.SendAsync(request,cancellationToken).ConfigureAwait(false);}}It is off by default: enabling it boxes and retains the arguments on every request (and therefore on any resulting
ApiException) for the lifetime of that request, so it adds a per-call allocation, keeps otherwise-collectable
arguments alive, and the captured values frequently contain credentials or PII - the same trade-offs as
CaptureRequestContent. Use RefitSettings.ExceptionRedactor to scrub the values before an exception reaches a logging
or telemetry pipeline.
The captured values are positional. Under the source-generated request path they are supplied as a bare
object?[] with no parameter names attached, so match them against your interface method's declared parameter order.
RestMethodInfo (with its MethodInfo.GetParameters()) is only published on the reflection path today, so the
name-recovery shown above applies there; the generated path does not currently surface RestMethodInfo.
Decorate a method with [Timeout(milliseconds)] to give that single call its own deadline, expressed in milliseconds:
publicinterfaceIGitHubApi{[Get("/users/{user}")][Timeout(5000)]// fail this call if it takes longer than 5 secondsTask<User>GetUser(stringuser);}When the deadline elapses the request is canceled and surfaces as an OperationCanceledException (typically a
TaskCanceledException), the same way a lapsed HttpClient.Timeout reports. If the method also declares a
CancellationToken parameter, both still work: whichever fires first — the caller's token or the timeout — cancels the
call.
The per-call timeout is independent of, and composes with, HttpClient.Timeout (the client-wide default) and any
timeout enforced by Polly or a DelegatingHandler; whichever deadline elapses first wins. A value that is not positive
leaves the method without a per-call deadline. Only a positive value adds one, so methods without [Timeout] pay no
extra cost.
Methods decorated with Multipart attribute will be submitted with multipart content type.
At this time, multipart methods support the following parameter types:
string(parameter name will be used as name and string value as value)Guid,DateTime,DateTimeOffset,TimeSpan(andDateOnly/TimeOnlyon supported targets) — sent as their plain text form (not JSON-quoted)- byte array
StreamFileInfo
Name of the field in the multipart data priority precedence:
multipartItem.Nameif specified and not null (optional); dynamic, allows naming form data part at execution time.[AliasAs]attribute (optional) that decorate the streamPart parameter in the method signature (see below); static, defined in code.MultipartItemparameter name (default) as defined in the method signature; static, defined in code.
A custom boundary can be specified with an optional string parameter to the Multipart attribute. If left empty, this
defaults to ----MyGreatBoundary.
To specify the file name and content type for byte array (byte[]), Stream and FileInfo parameters, use of a
wrapper class is required.
The wrapper classes for these types are ByteArrayPart, StreamPart and FileInfoPart.
publicinterfaceISomeApi{[Multipart][Post("/users/{id}/photo")]TaskUploadPhoto(intid,[AliasAs("myPhoto")]StreamPartstream);}To pass a Stream to this method, construct a StreamPart object like so:
someApiInstance.UploadPhoto(id,newStreamPart(myPhotoStream,"photo.jpg","image/jpeg"));Note: The AttachmentName attribute that was previously described in this section has been deprecated and its use is
not recommended.
By default a complex object passed to a multipart method is added as a single serialized part (for example one
application/json body) named after the parameter. That single part cannot be bound field by field by a server that
expects form fields (such as an ASP.NET [FromForm] model).
Opt in to per-property flattening by decorating the parameter with [FormObject]. Each public property is then sent as
its own multipart/form-data text field:
publicclassSetupModel{[AliasAs("full_name")]publicstringName{get;set;}publicintAge{get;set;}publicstring[]Roles{get;set;}}publicinterfaceIUploadApi{[Multipart][Post("/setup")]TaskSetupAsync([FormObject]SetupModelmodel,[AliasAs("recipe")]StreamPartrecipe);}The call above sends full_name, Age and Roles as individual form fields alongside the recipe file part.
- Field names are resolved per property:
[AliasAs]first, then the content serializer's field name (for example[JsonPropertyName]), then theUrlParameterKeyFormatter. - Values are rendered as plain text through the
FormUrlEncodedParameterFormatter, and collections are joined per theCollectionFormat(the default joins with commas), mirroring url-encoded body flattening. - Nested objects are flattened into composed
parent.childfield names (bounded by a nesting-depth cap and a reference-cycle guard). - Files (
byte[],Stream,FileInfoand theByteArrayPart/StreamPart/FileInfoPartwrappers) are not converted by[FormObject]; keep passing those as their own separate part parameters.
Without [FormObject] the existing single-serialized-part behaviour is unchanged.
Note that in Refit unlike in Retrofit, there is no option for a synchronous
network request - all requests must be async, either via Task or via
IObservable. There is also no option to create an async method via a Callback
parameter unlike Retrofit, because we live in the async/await future.
Similarly to how body content changes via the parameter type, the return type will determine the content returned.
Returning Task without a type parameter will discard the content and solely tell you whether or not the call succeeded:
[Post("/users/new")]TaskCreateUser([Body]Useruser);// This will throw if the network call failsawaitCreateUser(someUser);If the type parameter is 'HttpResponseMessage' or 'string', the raw response message or the content as a string will be returned respectively.
// Returns the content as a string (i.e. the JSON data)[Get("/users/{user}")]Task<string>GetUser(stringuser);// Returns the raw response, as an IObservable that can be used with the// Reactive Extensions[Get("/users/{user}")]IObservable<HttpResponseMessage>GetUser(stringuser);There is also a generic wrapper class called ApiResponse<T> that can be used as a return type. Using this class as a
return type allows you to retrieve not just the content as an object, but also any metadata associated with the
request/response. This includes information such as response headers, the http status code and reason phrase (e.g. 404
Not Found), the response version, the original request message that was sent and in the case of an error, an
ApiException object containing details of the error. Following are some examples of how you can retrieve the response
metadata.
//Returns the content within a wrapper class containing metadata about the request/response[Get("/users/{user}")]Task<ApiResponse<User>>GetUser(stringuser);//Calling the APIvarresponse=awaitgitHubApi.GetUser("octocat");//Determining if a success status code was received and there wasn't any other error//(for example, during content deserialization)if(response.IsSuccessful){//YAY! Do the thing...}if(response.IsReceived){//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)varhttpStatus=response.StatusCode;//Retrieving a well-known header value (e.g. "Server" header)varserverHeaderValue=response.Headers.Server!=null?response.Headers.Server.ToString():string.Empty;//Retrieving a custom header valuevarcustomHeaderValue=string.Join(',',response.Headers.GetValues("A-Custom-Header"));//Looping through all the headersforeach(varheaderinresponse.Headers){varheaderName=header.Key;varheaderValue=string.Join(',',header.Value);}//Finally, retrieving the content in the response body as a strongly-typed objectvaruser=response.Content;}A successful response does not imply a response body. Per RFC 9110 a
204 (No Content), a 304 (Not Modified), a response to HEAD, or any 2xx with a zero-length or literal null body
deserializes to Content == nullwithout an error — so IsSuccessful / IsSuccessStatusCode being true tells you
the request succeeded, not that Content is present. Use one of the content-presence members below when you need the
deserialized body:
HasContent— the covariance-safe way to null-checkContenton its own; whentruethe compiler knowsContentis non-null.IsSuccessfulWithContent— a single check that combines success and content presence (IsSuccessful && Content is not null); whentruethe compiler knowsContentis non-null.
if(response.HasContent){// response.Content is non-null hereProcess(response.Content);}// Or, when you want "succeeded and has a body" in one narrowing check:if(response.IsSuccessfulWithContent){// response.Content is non-null hereProcess(response.Content);}EnsureSuccessStatusCodeAsync() and EnsureSuccessfulAsync() are available directly on IApiResponse<T> (not just the
concrete ApiResponse<T>), throwing the captured ApiException / ApiRequestException when the request was not
successful:
IApiResponse<User>response=awaitgitHubApi.GetUser("octocat");awaitresponse.EnsureSuccessStatusCodeAsync();The same two guards are available on the non-generic IApiResponse (for endpoints that return no deserialized body), so
you can throw the captured error without inspecting IsSuccessStatusCode / Error by hand:
IApiResponseresponse=awaitgitHubApi.DeleteUser("octocat");awaitresponse.EnsureSuccessStatusCodeAsync();// throws ApiException / ApiRequestException on failureApiResponse<T> (and IApiResponse / IApiResponse<T>) implement IDisposable, but for the common case you do not
need to dispose them and you are not leaking sockets. For every return type except the streaming ones below, Refit
buffers the body into memory, returns the connection to the pool, and disposes the underlying HttpResponseMessage
before handing the result back to you. Calling Dispose() (or a using) on those responses is a harmless, idempotent
no-op — fine to keep to satisfy analyzers, but not required.
Disposal genuinely matters only when you ask Refit for the live response, where the body is not buffered and you own the connection until you dispose it:
HttpResponseMessage,HttpContent,StreamApiResponse<HttpResponseMessage>,ApiResponse<HttpContent>,ApiResponse<Stream>
// Streaming: you own the response — dispose it (e.g. with `using`)[Get("/files/{id}")]Task<ApiResponse<Stream>>DownloadAsync(stringid);usingvarresponse=awaitapi.DownloadAsync("42");awaitusingvarstream=response.Content;// released when the response is disposedThe rule is decided in RestMethodInfoInternal.DetermineIfResponseMustBeDisposed: those three types (and their
ApiResponse<> wrappers) are the only ones Refit does not auto-dispose for you.
Beyond the built-in shapes, you can teach Refit to surface any return type by implementing
IReturnTypeAdapter<TReturn, TResult>. TReturn is the type your interface method returns; TResult is what the HTTP
call materializes (the deserialized body). The single Adapt method receives a deferred call and returns the wrapper:
// A cold, deferred call that runs only when invoked.publicsealedclassDeferredCall<T>(Func<CancellationToken,Task<T>>invoke){publicTask<T>InvokeAsync(CancellationTokenct=default)=>invoke(ct);}publicsealedclassDeferredCallAdapter<T>:IReturnTypeAdapter<DeferredCall<T>,T>{publicDeferredCall<T>Adapt(Func<CancellationToken,Task<T>>invoke)=>new(invoke);}publicinterfaceIUserApi{[Get("/users/{id}")]DeferredCall<User>GetUser(intid);// surfaced by the adapter above}- Generated (Native AOT / trimming friendly): the source generator discovers adapters declared in your project at
compile time and emits a direct
Adaptcall — no reflection or registration needed. - Reflection (
RestService.Forwithout the generator, or adapters from another assembly): register the adapter type on the settings —settings.ReturnTypeAdapters.Add(typeof(DeferredCallAdapter<>)).
A generic adapter's single type parameter is treated as the wrapped result type (Adapter<T> : IReturnTypeAdapter<Wrapper<T>, T>),
so Wrapper<User> closes it over User. Adapters must have a public parameterless constructor. The generated path builds
the request eagerly and captures it, so a generated deferred call is single-use; the reflection path rebuilds the
request on each invocation, so it can re-run.
Sometimes you want the fully built HttpRequestMessage — to inspect it, sign it, log it, or dispatch it yourself — without
Refit sending it. Declare the method to return Task<HttpRequestMessage> and Refit runs the whole request-building path
(URL, headers, and body/multipart content) and hands the request back instead of dispatching it. This is the equivalent of
Retrofit's Call.request(), and it removes the need for the old DelegatingHandler-mock workaround.
publicinterfaceIUserApi{[Get("/users/{id}")]Task<HttpRequestMessage>BuildGetUser(intid,[Query]stringfilter);[Post("/users")]Task<HttpRequestMessage>BuildCreateUser([Body]Useruser);}varapi=RestService.For<IUserApi>("https://api.example.com");usingvarrequest=awaitapi.BuildGetUser(42,"active");// request.Method -> GET// request.RequestUri -> /users/42?filter=active (relative; the HttpClient merges the base address on send)// request.Headers -> the built headers// dispatch it yourself, or inspect/sign/log it firstNotes:
- The caller owns the returned request — Refit does not dispose it (and does not dispose its content), so its body
stays readable and you can dispatch it yourself. Wrap it in
using(or dispose it) once you are done. - Only
Task<HttpRequestMessage>is supported (not a synchronousHttpRequestMessage), because request building is asynchronous — body serialization in particular. ACancellationTokenparameter is still allowed. - Both the source-generated and reflection request paths build byte-identical requests.
- A configured async
AuthorizationHeaderValueGetterruns at dispatch time, so it is not applied to a request obtained this way; add its header yourself if you dispatch the request manually.
When using something like ASP.NET Web API, it's a fairly common pattern to have a whole stack of CRUD REST services. Refit now supports these, allowing you to define a single API interface with a generic type:
publicinterfaceIReallyExcitingCrudApi<T,inTKey>whereT:class{[Post("")]Task<T>Create([Body]Tpayload);[Get("")]Task<List<T>>ReadAll();[Get("/{key}")]Task<T>ReadOne(TKeykey);[Put("/{key}")]TaskUpdate(TKeykey,[Body]Tpayload);[Delete("/{key}")]TaskDelete(TKeykey);}Which can be used like this:
// The "/users" part here is kind of important if you want it to work for more// than one type (unless you have a different domain for each type)varapi=RestService.For<IReallyExcitingCrudApi<User,string>>("http://api.example.com/users");When multiple services that need to be kept separate share a number of APIs, it is possible to leverage interface inheritance to avoid having to define the same Refit methods multiple times in different services:
publicinterfaceIBaseService{[Get("/resources")]Task<Resource>GetResource(stringid);}publicinterfaceIDerivedServiceA:IBaseService{[Delete("/resources")]TaskDeleteResource(stringid);}publicinterfaceIDerivedServiceB:IBaseService{[Post("/resources")]Task<string>AddResource([Body]Resourceresource);}In this example, the IDerivedServiceA interface will expose both the GetResource and DeleteResource APIs, while
IDerivedServiceB will expose GetResource and AddResource.
The generator walks the full interface hierarchy, so you can split a large API by resource area into focused interfaces and then aggregate them into a single client that declares nothing of its own:
publicinterfaceIUsersApi{[Get("/users/{user}")]Task<User>GetUser(stringuser);}publicinterfaceIReposApi{[Get("/users/{user}/repos")]Task<List<Repo>>GetRepos(stringuser);}// The aggregate client has no members of its own; it just composes the two APIs.publicinterfaceIGitHubApi:IUsersApi,IReposApi;The composed client exposes every method from both base interfaces:
varapi=RestService.For<IGitHubApi>("https://api.github.com");varuser=awaitapi.GetUser("octocat");// from IUsersApivarrepos=awaitapi.GetRepos("octocat");// from IReposApiIt works the same way with HttpClientFactory and dependency injection:
builder.Services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.github.com"));Each sub-interface (IUsersApi, IReposApi) can also be registered independently if some consumers only need one area,
and the header-attribute precedence rules described just below still apply to the composed client.
When using inheritance, existing header attributes will be passed along as well, and the inner-most ones will have precedence:
[Headers("User-Agent: AAA")]publicinterfaceIAmInterfaceA{[Get("/get?result=Ping")]Task<string>Ping();}[Headers("User-Agent: BBB")]publicinterfaceIAmInterfaceB:IAmInterfaceA{[Get("/get?result=Pang")][Headers("User-Agent: PANG")]Task<string>Pang();[Get("/get?result=Foo")]Task<string>Foo();}Here, IAmInterfaceB.Pang() will use PANG as its user agent, while IAmInterfaceB.Foo and IAmInterfaceB.Ping will
use BBB.
Note that if IAmInterfaceB didn't have a header attribute, Foo would then use the AAA value inherited from
IAmInterfaceA.
If an interface is inheriting more than one interface, the order of precedence is the same as the one in which the
inherited interfaces are declared:
publicinterfaceIAmInterfaceC:IAmInterfaceA,IAmInterfaceB{[Get("/get?result=Foo")]Task<string>Foo();}Here IAmInterfaceC.Foo would use the header attribute inherited from IAmInterfaceA, if present, or the one inherited
from IAmInterfaceB, and so on for all the declared interfaces.
Starting with C# 8.0, default interface methods (a.k.a. DIMs) can be defined on interfaces. Refit interfaces can provide additional logic using DIMs, optionally combined with private and/or static helper methods:
publicinterfaceIApiClient{// implemented by Refit but not exposed publicly[Get("/get")]internalTask<string>GetInternal();// Publicly available with added logic applied to the result from the API callpublicasyncTask<string>Get()=>FormatResponse(awaitGetInternal());privatestaticStringFormatResponse(stringresponse)=>$"The response is: {response}";}The type generated by Refit will implement the method IApiClient.GetInternal. If additional logic is required
immediately before or after its invocation, it shouldn't be exposed directly and can thus be hidden from consumers by
being marked as internal.
The default interface method IApiClient.Get will be inherited by all types implementing IApiClient, including - of
course - the type generated by Refit.
Consumers of the IApiClient will call the public Get method and profit from the additional logic provided in its
implementation (optionally, in this case, with the help of the private static helper FormatResponse).
To support runtimes without DIM-support (.NET Core 2.x and below or .NET Standard 2.0 and below), two additional types
would be required for the same solution.
internalinterfaceIApiClientInternal{[Get("/get")]Task<string>Get();}publicinterfaceIApiClient{publicTask<string>Get();}internalclassApiClient:IApiClient{privatereadonlyIApiClientInternalclient;publicApiClient(IApiClientInternalclient)=>this.client=client;publicasyncTask<string>Get()=>FormatResponse(awaitclient.Get());privatestaticStringFormatResponse(stringresponse)=>$"The response is: {response}";}Refit has first class support for IHttpClientFactory. Add a reference to Refit.HttpClientFactory
and call
the provided extension method in your ConfigureServices method to configure your Refit interface:
services.AddRefitClient<IWebApi>().ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));// Add additional IHttpClientBuilder chained methods as required here:// .AddHttpMessageHandler<MyHandler>()// .SetHandlerLifetime(TimeSpan.FromMinutes(2));Optionally, a RefitSettings object can be included:
varsettings=newRefitSettings();// Configure refit settings hereservices.AddRefitClient<IWebApi>(settings).ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));// Add additional IHttpClientBuilder chained methods as required here:// .AddHttpMessageHandler<MyHandler>()// .SetHandlerLifetime(TimeSpan.FromMinutes(2));// or injected from the containerservices.AddRefitClient<IWebApi>(provider =>newRefitSettings(){/* configure settings */}).ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));// Add additional IHttpClientBuilder chained methods as required here:// .AddHttpMessageHandler<MyHandler>()// .SetHandlerLifetime(TimeSpan.FromMinutes(2));Note that some of the properties of RefitSettings will be ignored because the HttpClient and HttpClientHandlers
will be managed by the HttpClientFactory instead of Refit.
Refit registers each client with IHttpClientFactory under a deterministic name. You can compute that same name with
UniqueName.ForType<T>(), for example to configure the underlying named HttpClient directly:
services.AddHttpClient(UniqueName.ForType<IWebApi>()).ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));If you would rather use a short, human-readable name, pass httpClientName to any AddRefitClient overload. That
name becomes the IHttpClientFactory client name and the client's default ILogger logging category, so it shortens
both without changing the assembly-qualified default that keeps registrations unique:
services.AddRefitClient<IWebApi>(settings:null,httpClientName:"web-api").ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));You can then get the api interface using constructor injection:
publicclassHomeController:Controller{publicHomeController(IWebApiwebApi){_webApi=webApi;}privatereadonlyIWebApi_webApi;publicasyncTask<IActionResult>Index(CancellationTokencancellationToken){varthing=await_webApi.GetSomethingWeNeed(cancellationToken);returnView(thing);}}If you split one API across several interfaces (for example ISomeSiteAuth and ISomeSiteData) but want them to
share a single underlying handler and connection pool, register them under the same client name. Interfaces that
resolve the same named HttpClient share the same IHttpClientFactory-managed, lifetime-rotated handler:
services.AddRefitClient<ISomeSiteAuth>((RefitSettings?)null,"somesite").ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));services.AddRefitClient<ISomeSiteData>((RefitSettings?)null,"somesite").ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));You do not need to do this for correctness. By default each interface gets its own factory-managed named client, and
IHttpClientFactory already pools and rotates the underlying handlers, so there is no socket exhaustion or DNS-staleness
concern. Sharing a name simply collapses the interfaces onto one pool (and one shared ConfigureHttpClient/handler
configuration) when they target the same host.
Outside DI you can share a single HttpClient directly by passing the same instance to multiple
RestService.For<T>(client) calls (see Providing a custom HttpClient below). Prefer
sharing the pooled handler over caching long-lived HttpClient instances yourself, so handler rotation and DNS refresh
keep working.
You can supply a custom HttpClient instance by simply passing it as a parameter to the RestService.For<T> method:
RestService.For<ISomeApi>(newHttpClient(){BaseAddress=newUri("https://www.someapi.com/api/")});However, when supplying a custom HttpClient instance, HttpMessageHandlerFactory will not be used because you already
control the handler pipeline.
AuthorizationHeaderValueGetter does still work with RestService.For<T>(httpClient, settings) when the request
includes an Authorization header placeholder (for example [Headers("Authorization: Bearer")]).
If you still want to be able to configure the HtttpClient instance that Refit provides while still making use of the
above settings, simply expose the HttpClient on the API interface:
interfaceISomeApi{// This will automagically be populated by Refit if the property existsHttpClientClient{get;}[Headers("Authorization: Bearer")][Get("/endpoint")]Task<string>SomeApiEndpoint();}Then, after creating the REST service, you can set any HttpClient property you want, e.g. Timeout:
SomeApi=RestService.For<ISomeApi>("https://www.someapi.com/api/",newRefitSettings(){AuthorizationHeaderValueGetter=(rq,ct)=>GetTokenAsync()});SomeApi.Client.Timeout=timeout;Refit's recommended source-generator-first setup for Native AoT and trimmed applications is:
- Use normal Refit interfaces so the Refit source generator produces the client implementation at build time.
- Use
RestService.ForGenerated<T>(...)when the application should require a source-generated client at runtime. Otherwise, preferRestService.For<T>(...)over reflection-heavy manual patterns aroundTypewhere possible. - Supply source-generated
System.Text.Jsonmetadata for your DTOs. - Do not reference the
Refit.Reflectionpackage. The reflection request builder is opt-in, so leaving it out keeps that pipeline out of your application entirely. Build with warnings as errors and the RF006 diagnostic will point at any method that still needs it.
For the default SystemTextJsonContentSerializer on .NET 8+, Refit prefers JsonTypeInfo metadata from your configured
JsonSerializerOptions when it is available. That means Native AoT apps can improve compatibility by supplying
source-generated metadata through a JsonSerializerContext or TypeInfoResolver on the serializer options they pass
into SystemTextJsonContentSerializer.
[JsonSerializable(typeof(Todo))]publicpartialclassTodoJsonContext:JsonSerializerContext{}varsettings=newRefitSettings(newSystemTextJsonContentSerializer(newJsonSerializerOptions(JsonSerializerDefaults.Web){TypeInfoResolver=TodoJsonContext.Default}));varapi=RestService.For<ITodoApi>("https://api.example.com",settings);AddRefitClient<T> is annotated with RequiresUnreferencedCode because it can fall back to the reflection-based
registration path. For Native AoT or trimmed applications that rely on the source generator, use
AddRefitGeneratedClient<T> (from Refit.HttpClientFactory) instead. It registers the client through
IHttpClientFactory and RestService.ForGenerated<T>, so there is no reflection fallback and no
RequiresUnreferencedCode warning, while the usual IHttpClientBuilder configuration (base address, message handlers,
resilience pipelines) still applies:
services.AddRefitGeneratedClient<IWebApi>().ConfigureHttpClient(c =>c.BaseAddress=newUri("https://api.example.com"));// optionally with settings, a settings factory resolved from the container, or a custom HttpClient nameservices.AddRefitGeneratedClient<IWebApi>(settings);services.AddRefitGeneratedClient<IWebApi>(provider =>newRefitSettings{/* configure settings */});services.AddRefitGeneratedClient<IWebApi>(settings,"my-named-client");If no source-generated client exists for the interface, resolving it throws an InvalidOperationException that points
you back to the generator output - the registration never silently falls back to reflection.
If a generated Refit client cannot be found at runtime, Refit now explicitly points you back to the source
generator/build output and recommends generated clients plus source-generated System.Text.Json metadata for Native AoT
scenarios.
Refit also ships analyzers for newer Roslyn toolchains, including a Roslyn 5.0 build for newer Visual Studio versions.
Refit has different exception handling behavior depending on if your Refit interface methods return Task<T> or if they
return Task<IApiResponse>, Task<IApiResponse<T>>, or Task<ApiResponse<T>>.
Task<ApiResponse<T>>
Refit traps any HttpRequestException or TaskCanceledException raised by the HttpClient in an
ApiRequestException.
Refit also traps any ApiException raised by the ExceptionFactory when processing the response, and any errors that
occur when attempting to deserialize the response to ApiResponse<T>.
In both cases, it will populate the exception into the Error property on ApiResponse<T> without throwing the
exception.
You can then decide what to do like so:
varresponse=await_myRefitClient.GetSomeStuff();if(response.IsSuccessful){//do your thing}else{// If you want to distinguish between request and response errorsif(response.HasRequestError(outvarrequestError))_logger.LogError(requestError,"An error occurred while sending the request.");elseif(response.HasResponseError(outvarresponseError))_logger.LogError(responseError,responseError.Content);// Or just log the error directly_logger.LogError(response.Error,"An error occurred while calling the API.");}Note
Migrating from v8-v11? response.Error.Content no longer compiles since v12. Error is now typed as
ApiExceptionBase? (request-side context only), so the response body moved to the derived ApiException. Read it via
response.HasResponseError(out var apiException) (as shown above) and then apiException.Content, or cast with
(response.Error as ApiException)?.Content. See the
V12.x.x breaking changes for the full before/after.
Note
The IsSuccessful property checks whether the response status code is in the range 200-299 and there wasn't any other
error (for example, during content deserialization). If you just want to check the HTTP response status code, you can
use the IsSuccessStatusCode property. Neither property implies a non-null Content — a successful response can have
no body (e.g. 204 No Content or a literal null body). Use HasContent or IsSuccessfulWithContent when you need the
deserialized Content to be present.
Refit throws any exception raised by the HttpClient and wraps it in an ApiRequestException.
It also throws any ApiException raised by the ExceptionFactory when processing the response and any errors that
occur when attempting to deserialize the response to Task<T>.
// ...try{varresult=awaitawesomeApi.GetFooAsync("bar");}catch(ApiRequestExceptionexception){//exception handling for when a response was not received from the server}catch(ApiExceptionexception){//exception handling for when a response was received from the server}// Or to not distinguish between request/response exceptionscatch(ApiExceptionBaseexception){//exception handling for when an error occurs during the request/response}// ...Refit can also throw ValidationApiException instead which in addition to the information present on ApiException
also contains ProblemDetails when the service implements the RFC 7807
specification for problem details and the response content type is application/problem+json
For specific information on the problem details of the validation exception, simply catch ValidationApiException:
// ...try{varresult=awaitawesomeApi.GetFooAsync("bar");}catch(ValidationApiExceptionvalidationException){// handle validation here by using validationException.Content,// which is type of ProblemDetails according to RFC 7807// If the response contains additional properties on the problem details,// they will be added to the validationException.Content.Extensions collection.}catch(ApiExceptionexception){// other exception handling}// ...ApiException exposes GetContentAsAsync<T>() to deserialize the error body, but it cannot be awaited inside an
exception filter (catch ... when ...), which the CLR requires to be synchronous. Because the body is already
buffered into the Content string, you can deserialize it synchronously with GetContentAs<T>() or the
non-throwing TryGetContentAs<T>(out T? content). This lets you route to a specific handler only when the error
body matches a well-known shape, and fall through to a generic handler otherwise:
try{varresult=awaitawesomeApi.GetFooAsync("bar");}catch(ApiExceptionexception)when(exception.TryGetContentAs<Error>(outvarerror)){// handle the strongly-typed `error` here}catch(ApiExceptionexception){// generic handling when the body was missing or not an `Error`}TryGetContentAs<T> returns false (and never throws) when there is no content, the content does not deserialize,
or the configured serializer cannot deserialize synchronously. GetContentAs<T> returns default when there is no
content and throws NotSupportedException when the configured IHttpContentSerializer does not implement
ISynchronousContentDeserializer. The built-in SystemTextJsonContentSerializer, NewtonsoftJsonContentSerializer,
and XmlContentSerializer all implement it; a custom serializer can opt in by implementing
ISynchronousContentDeserializer.
By default HttpClient disposes the request content once a request is sent, so the body cannot be read back from
ApiException.RequestMessage.Content. Set RefitSettings.CaptureRequestContent = true to buffer the request body
into a string before sending and expose it on ApiExceptionBase.RequestContent:
varsettings=newRefitSettings{CaptureRequestContent=true};varawesomeApi=RestService.For<IAwesomeApi>("https://api.example.com",settings);try{awaitawesomeApi.PostFooAsync(payload);}catch(ApiExceptionexception)when(exception.HasRequestContent){// exception.RequestContent holds exactly what Refit sent on the wirelogger.LogError("Failed request body: {Body}",exception.RequestContent);}CaptureRequestContent defaults to false because it buffers the entire body into memory; avoid it for large or
streamed uploads. When it is disabled, HasRequestContent is false and RequestContent is null.
You can also override default exceptions behavior that are raised by the ExceptionFactory when processing the result
by providing a custom exception factory in RefitSettings. For example, you can suppress all ApiExceptions with the
following:
varnullTask=Task.FromResult<Exception>(null);vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{ExceptionFactory= httpResponse =>nullTask;});For exceptions raised when attempting to deserialize the response use DeserializationExceptionFactory described bellow.
You can override default deserialization exceptions behavior that are raised by the DeserializationExceptionFactory
when processing the result by providing a custom exception factory in RefitSettings. For example, you can suppress all
deserialization exceptions with the following:
varnullTask=Task.FromResult<Exception>(null);vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{DeserializationExceptionFactory=(httpResponse,exception)=>nullTask;});You can control the exception that is surfaced when HttpClient.SendAsync throws a transport-level error (for example,
HttpRequestException, SocketException, or TaskCanceledException) by setting TransportExceptionFactory on
RefitSettings. The factory receives the HttpRequestMessage that was being sent, the raw exception, and the
CancellationToken that was passed to the call, and returns the exception that Refit will ultimately throw
(non-ApiResponse path) or store in IApiResponse.Error (ApiResponse path).
By default, Refit wraps the transport exception in an ApiRequestException that carries the full request context,
except when the exception is an OperationCanceledException and the CancellationToken is already cancelled — in
that case the original exception is returned unchanged. You can replace that behavior entirely, for example to
re-throw the original exception unchanged in all cases:
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{TransportExceptionFactory=(request,ex,ct)=>ex});Because RefitSettings is captured in the closure, you can also reconstruct the default behavior with your own
adjustments:
varsettings=newRefitSettings();settings.TransportExceptionFactory=(request,ex,ct)=>{// Pass cancellation through so ApiResponse callers still throw rather// than capturing a cancelled request as a soft error.if(exisOperationCanceledException&&ct.IsCancellationRequested)returnex;// Wrap everything else in ApiRequestException, just like the default.returnnewApiRequestException(request,request.Method,settings,ex);};vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",settings);For users of Serilog, you can enrich the logging of ApiException using the
Serilog.Exceptions.Refit NuGet package. Details of how to
integrate this package into your applications can be
found here.
The first-party Refit.Testing package lets you test the Refit
clients your app depends on without a mocking library or a live server. You describe the calls you expect as a
route table — each entry pairs a Route (which request to match) with a Reply (what to send back) — and point
a real Refit client at it:
usingRefit.Testing;varhttp=newStubHttp{{Route.Get("/users/{id}"),Reply.With(newUser(7,"octocat"))},{Route.Post("/users"),Reply.Status(HttpStatusCode.Created)},};varapi=http.CreateClient<IGitHubApi>("https://api.github.com");varuser=awaitapi.GetUser(7);// assert the body your client sent, as a typed object:varsent=awaithttp.LastRequestBodyAsync<NewUser>();awaithttp.VerifyAllCalledAsync();Because the handler is Refit-aware, route templates mirror your [Get("/users/{id}")] attributes, Reply.With<T>
serializes with the client's own serializer (no hand-written JSON), and you can read the sent request body back as a
typed object. It also ships NetworkBehavior for seeded latency/fault injection and StubApiResponse<T> for
unit-testing code that consumes IApiResponse<T> directly.
See the testing guide for the full walkthrough.
