- Notifications
You must be signed in to change notification settings - Fork749
The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.
License
MIT, MIT licenses found
Licenses found
reactiveui/refit
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Refit | Refit.HttpClientFactory | Refit.Newtonsoft.Json | |
---|---|---|---|
NuGet |
Refit is a library heavily inspired by Square'sRetrofit library, and it turns your RESTAPI into a live interface:
publicinterfaceIGitHubApi{[Get("/users/{user}")]Task<User>GetUser(stringuser);}
TheRestService
class generates an implementation ofIGitHubApi
that usesHttpClient
to make its calls:
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com");varoctocat=awaitgitHubApi.GetUser("octocat");
.NET Core supports registering via HttpClientFactory
services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c=>c.BaseAddress=newUri("https://api.github.com"));
- Where does this work?
- API Attributes
- Querystrings
- Body content
- Setting request headers
- Passing state into DelegatingHandlers
- Multipart uploads
- Retrieving the response
- Using generic interfaces
- Interface inheritance
- Default Interface Methods
- Using HttpClientFactory
- Providing a custom HttpClient
- Handling exceptions
Refit currently supports the following platforms and any .NET Standard 2.0 target:
- UWP
- Xamarin.Android
- Xamarin.Mac
- Xamarin.iOS
- Desktop .NET 4.6.1
- .NET 6 / 8
- Blazor
- Uno Platform
Fixes for some issues experianced, this lead to some breaking changesSeeReleases for full details.
Refit 6 requires Visual Studio 16.8 or higher, or the .NET SDK 5.0.100 or higher. It can target any .NET Standard 2.0 platform.
Refit 6 does not support the oldpackages.config
format for NuGet references (as they do not support analyzers/source generators). You mustmigrate to PackageReference to use Refit v6 and later.
Refit 6 makesSystem.Text.Json the default JSON serializer. If you'd like to continue to useNewtonsoft.Json
, add theRefit.Newtonsoft.Json
NuGet package and set yourContentSerializer
toNewtonsoftJsonContentSerializer
on yourRefitSettings
instance.System.Text.Json
is faster and uses less memory, though not all features are supported. Themigration guide contains more details.
IContentSerializer
was renamed toIHttpContentSerializer
to better reflect its purpose. Additionally, two of its methods were renamed,SerializeAsync<T>
->ToHttpContent<T>
andDeserializeAsync<T>
->FromHttpContentAsync<T>
. Any existing implementations of these will need to be updated, though the changes should be minor.
Refit 6.3 splits out the XML serialization viaXmlContentSerializer
into a separate package,Refit.Xml
. Thisis to reduce the dependency size when using Refit with Web Assembly (WASM) applications. If you require XML, add a referencetoRefit.Xml
.
Every method must have an HTTP attribute that provides the request method andrelative URL. There are six built-in annotations: Get, Post, Put, Delete, Patch andHead. 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 andparameters on the method. A replacement block is an alphanumeric stringsurrounded by { and }.
If the name of your parameter doesn't match the name in the URL path, use theAliasAs
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;}}
Parameters that are not specified as a URL substitution will automatically beused as query parameters. This is different than Retrofit, where allparameters must be explicitly specified.
The comparison between parameter name and URL parameter isnotcase-sensitive, so it will work correctly if you name your parametergroupId
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"
If you specify anobject
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 theQuery
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 theAliasAs
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);
WhereTweetParams
is a POCO, and properties will also support[AliasAs]
attributes.
Use theQuery
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 inRefitSettings
, that will be used by default, unless explicitly defined inQuery
attribute.
vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{CollectionFormat=CollectionFormat.Multi});
Use theQueryUriFormat
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"
Formatting Keys
To customize the format of query keys, you have two main options:
Using the
AliasAs
Attribute:You can use the
AliasAs
attribute to specify a custom key name for a property. This attribute will always take precedence over any key formatter you specify.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=10
Using the
RefitSettings.UrlParameterKeyFormatter
Property:By default, Refit uses the property name as the query key without any additional formatting. If you want to apply a custom format across all your query keys, you can use the
UrlParameterKeyFormatter
property. Remember that if a property has anAliasAs
attribute, 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: TheAliasAs
attribute always takes the top priority. If both the attribute and a custom key formatter are present, theAliasAs
attribute's value will be used.
In Refit, theUrlParameterFormatter
property withinRefitSettings
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.
UsingUrlParameterFormatter
:
Assign a custom formatter that implements theIUrlParameterFormatter
interface to theUrlParameterFormatter
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 aDateTime
parameter is encountered, it formats the date asyyyyMMdd
.
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 theUrlParameterFormatter
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.
One of the parameters in your method can be used as the body, by using theBody attribute:
[Post("/users/new")]TaskCreateUser([Body]Useruser);
There are four possibilities for supplying the body data, depending on thetype 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 (seeform posts below) - For all other types, the object will be serialized using the content serializer specified inRefitSettings (JSON is the default).
By default, Refit streams the body content without buffering it. This means you canstream a file from disk, for example, without incurring the overhead of loadingthe whole file into memory. The downside of this is that noContent-Length
headeris seton the request. If your API needs you to send aContent-Length
header withthe request, you can disable this streaming behavior by setting thebuffered
argumentof the[Body]
attribute totrue
:
TaskCreateUser([Body(buffered:true)]Useruser);
JSON requests and responses are serialized/deserialized using an instance of theIHttpContentSerializer
interface. Refit provides two implementations out of the box:SystemTextJsonContentSerializer
(which is the default JSON serializer) andNewtonsoftJsonContentSerializer
. The first usesSystem.Text.Json
APIs and is focused on high performance and low memory usage, while the latter uses the knownNewtonsoft.Json
library and is more versatile and customizable. You can read more about the two serializers and the main differences between the twoat this link.
For instance, here is how to create a newRefitSettings
instance using theNewtonsoft.Json
-based serializer (you'll also need to add aPackageReference
toRefit.Newtonsoft.Json
):
varsettings=newRefitSettings(newNewtonsoftJsonContentSerializer());
If you're usingNewtonsoft.Json
APIs, you can customize their behavior by setting theNewtonsoft.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. Itmight be beneficial to isolate the settings for calls to a particular API.When creating a Refit generated live interface, you may optionally pass aRefitSettings
that will allow you to specify what serializer settings youwould like. This allows you to have different serializer settings for separateAPIs:
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 customised using Json.NET'sJsonProperty attribute:
publicclassFoo{// Works like [AliasAs("b")] would in form posts (see below)[JsonProperty(PropertyName="b")]publicstringBar{get;set;}}
To apply the benefits of the newJSON source generator for System.Text.Json added in .NET 6, you can useSystemTextJsonContentSerializer
with a custom instance ofRefitSettings
andJsonSerializerOptions
:
varoptions=newJsonSerializerOptions(){TypeInfoResolver=MyJsonSerializerContext.Default};vargitHubApi=RestService.For<IGitHubApi>("https://api.github.com",newRefitSettings{ContentSerializer=newSystemTextJsonContentSerializer(options)});
XML requests and responses are serialized/deserialized usingSystem.Xml.Serialization.XmlSerializer.By default, Refit will use JSON content serialization, to use XML content configure the ContentSerializer to use theXmlContentSerializer
:
vargitHubApi=RestService.For<IXmlApi>("https://www.w3.org/XML",newRefitSettings{ContentSerializer=newXmlContentSerializer()});
Property serialization/deserialization can be customised using attributes found in theSystem.Xml.Serialization namespace:
publicclassFoo{[XmlElement(Namespace="https://www.w3.org/XML")]publicstringBar{get;set;}}
TheSystem.Xml.Serialization.XmlSerializer provides many options for serializing, those options can be set by providing anXmlContentSerializerSettings
to theXmlContentSerializer
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 asapplication/x-www-form-urlencoded
),initialize the Body attribute withBodySerializationMethod.UrlEncoded
.
The parameter can be anIDictionary
:
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 allpublic, readable properties willbe serialized as form fields in the request. This approach allows you to aliasproperty names using[AliasAs("whatever")]
which can help if the API hascryptic 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 asone=value1&two=value2
:
publicclassSomeObject{[JsonProperty(PropertyName="one")]publicstringFirstProperty{get;set;}[JsonProperty(PropertyName="notTwo")][AliasAs("two")]publicstringSecondProperty{get;set;}}
NOTE: This use ofAliasAs
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")]
.
You can set one or more static request headers for a request applying aHeaders
attribute to the method:
[Headers("User-Agent: Awesome Octocat App")][Get("/users/{user}")]Task<User>GetUser(stringuser);
Static headers can also be added toevery request in the API by applying theHeaders
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 headerwith a dynamic value to a request by applying aHeader
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 anAuthorization
header is such a common use case that you can add an access token to a request by applying anAuthorize
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 provided
If you need to set multiple headers at runtime, you can add aIDictionary<string, string>
and apply aHeaderCollection
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
AuthorizationHeaderValueGetter
in theRefitSettings
instance. Refit will call your delegate each time it needs to obtain the token, so it's a good idea for your mechanism to cache the token value for some period within the token lifetime.
Although we make provisions for adding dynamic headers at runtime directly in Refit,most use-cases would likely benefit from registering a customDelegatingHandler
in order to inject the headers as part of theHttpClient
middleware pipelinethus removing the need to add lots of[Header]
or[HeaderCollection]
attributes.
In the example above we are leveraging a[HeaderCollection]
parameter to inject anAuthorization
andX-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 throughsome interfaceITenantProvider
and has a data storeIAuthTokenStore
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 thing
Unlike Retrofit, where headers do not overwrite each other and are all added tothe request regardless of how many times the same header is defined, Refit takesa similar approach to the approach ASP.NET MVC takes with action filters —redefining a header will replace it, in the following order of precedence:
Headers
attribute on the interface(lowest priority)Headers
attribute on the methodHeader
attribute orHeaderCollection
attribute 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 headerswith 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 redefininga static header without a value (i.e. without: <value>
) or passingnull
fora 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,"");
If there is runtime state that you need to pass to aDelegatingHandler
you can add a property with a dynamic value to the underlyingHttpRequestMessage.Properties
by applying aProperty
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 theHttpRequestMessage.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 inHttpRequestMessage.Properties
will be overwritten.The parameter itself can be anyobject
. Properties can be accessed inside aDelegatingHandler
as follows:
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 5HttpRequestMessage.Properties
has been markedObsolete
and Refit will instead populate the value into the newHttpRequestMessage.Options
.
Because Refit supportsHttpClientFactory
it is possible to configure Polly policies on your HttpClient.If your policy makes use ofPolly.Context
this can be passed via Refit by adding[Property("PolicyExecutionContext")] Polly.Context context
as behind the scenesPolly.Context
is simply stored inHttpRequestMessage.Properties
under the keyPolicyExecutionContext
and is of typePolly.Context
. It's only recommended to pass thePolly.Context
this way if your use case requires that thePolly.Context
be initialized with dynamic content only known at runtime. If yourPolly.Context
only requires the same content every time (e.g anILogger
that you want to use to log from inside your policies) a cleaner approach is to inject thePolly.Context
via aDelegatingHandler
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 youhave 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. TheRestMethodInfo
contains more information about the method being called such as the fullMethodInfo
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(HttpRequestMessageOptions.RestMethodInfoKey,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 5HttpRequestMessage.Properties
has been markedObsolete
and Refit will instead populate the value into the newHttpRequestMessage.Options
. Refit providesHttpRequestMessageOptions.InterfaceTypeKey
andHttpRequestMessageOptions.RestMethodInfoKey
to respectively access the interface type and REST method info from the options.
Methods decorated withMultipart
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)
- byte array
- Stream
- FileInfo
Name of the field in the multipart data priority precedence:
- multipartItem.Name if 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.
- MultipartItem parameter name (default) as defined in the method signature; static, defined in code.
A custom boundary can be specified with an optional string parameter to theMultipart
attribute. If left empty, this defaults to----MyGreatBoundary
.
To specify the file name and content type for byte array (byte[]
),Stream
andFileInfo
parameters, use of a wrapper class is required.The wrapper classes for these types areByteArrayPart
,StreamPart
andFileInfoPart
.
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.
Note that in Refit unlike in Retrofit, there is no option for a synchronousnetwork request - all requests must be async, either viaTask
or viaIObservable
. There is also no option to create an async method via a Callbackparameter unlike Retrofit, because we live in the async/await future.
Similarly to how body content changes via the parameter type, the return typewill determine the content returned.
Returning Task without a type parameter will discard the content and solelytell 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 responsemessage 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 calledApiResponse<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, anApiException
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");//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)varhttpStatus=response.StatusCode;//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...}//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;
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, theIDerivedServiceA
interface will expose both theGetResource
andDeleteResource
APIs, whileIDerivedServiceB
will exposeGetResource
andAddResource
.
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 usePANG
as its user agent, whileIAmInterfaceB.Foo
andIAmInterfaceB.Ping
will useBBB
.Note that ifIAmInterfaceB
didn't have a header attribute,Foo
would then use theAAA
value inherited fromIAmInterfaceA
.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();}
HereIAmInterfaceC.Foo
would use the header attribute inherited fromIAmInterfaceA
, if present, or the one inherited fromIAmInterfaceB
, 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 methodIApiClient.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 asinternal
.The default interface methodIApiClient.Get
will be inherited by all types implementingIApiClient
, including - of course - the type generated by Refit.Consumers of theIApiClient
will call the publicGet
method and profit from the additional logic provided in its implementation (optionally, in this case, with the help of the private static helperFormatResponse
).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 the ASP.Net Core 2.1 HttpClientFactory. Add a reference toRefit.HttpClientFactory
and callthe provided extension method in yourConfigureServices
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, aRefitSettings
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 ofRefitSettings
will be ignored because theHttpClient
andHttpClientHandlers
will be managed by theHttpClientFactory
instead of Refit.
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);}}
You can supply a customHttpClient
instance by simply passing it as a parameter to theRestService.For<T>
method:
RestService.For<ISomeApi>(newHttpClient(){BaseAddress=newUri("https://www.someapi.com/api/")});
However, when supplying a customHttpClient
instance the followingRefitSettings
properties will not work:
AuthorizationHeaderValueGetter
HttpMessageHandlerFactory
If you still want to be able to configure theHtttpClient
instance thatRefit
provides while still making use of the above settings, simply expose theHttpClient
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 anyHttpClient
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 has different exception handling behavior depending on if your Refit interface methods returnTask<T>
or if they returnTask<IApiResponse>
,Task<IApiResponse<T>>
, orTask<ApiResponse<T>>
.
Refit traps anyApiException
raised by theExceptionFactory
when processing the response, and any errors that occur when attempting to deserialize the response toApiResponse<T>
, and populates the exception into theError
property onApiResponse<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{_logger.LogError(response.Error,response.Error.Content);}
Note
TheIsSuccessful
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 theIsSuccessStatusCode
property.
Refit throws anyApiException
raised by theExceptionFactory
when processing the response and any errors that occur when attempting to deserialize the response toTask<T>
.
// ...try{varresult=awaitawesomeApi.GetFooAsync("bar");}catch(ApiExceptionexception){//exception handling}// ...
Refit can also throwValidationApiException
instead which in addition to the information present onApiException
also containsProblemDetails
when the service implements theRFC 7807 specification for problem details and the response content type isapplication/problem+json
For specific information on the problem details of the validation exception, simply catchValidationApiException
:
// ...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}// ...
You can also override default exceptions behavior that are raised by theExceptionFactory
when processing the result by providing a custom exception factory inRefitSettings
. For example, you can suppress all exceptions 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 theDeserializationExceptionFactory
when processing the result by providing a custom exception factory inRefitSettings
. 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;});
For users ofSerilog, you can enrich the logging ofApiException
using theSerilog.Exceptions.Refit NuGet package. Details of how tointegrate this package into your applications can be foundhere.
About
The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.