Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

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

MIT
LICENSE
MIT
COPYING
NotificationsYou must be signed in to change notification settings

reactiveui/refit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Refit

Refit: The automatic type-safe REST library for .NET Core, Xamarin and .NET

Buildcodecov

RefitRefit.HttpClientFactoryRefit.Newtonsoft.Json
NuGetNuGetNuGetNuGet

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"));

Table of Contents

Where does this work?

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

SDK Requirements

Updates in 8.0.x

Fixes for some issues experianced, this lead to some breaking changesSeeReleases for full details.

V6.x.x

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.

Breaking changes in 6.x

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.

Updates in 6.3

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.

API Attributes

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 parametergroupIdin 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"

Querystrings

Dynamic Querystring Parameters

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.

Collections as Querystring parameters

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});

Unescape Querystring parameters

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"

Custom Querystring parameter formatting

Formatting Keys

To customize the format of query keys, you have two main options:

  1. Using theAliasAs Attribute:

    You can use theAliasAs 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
  2. Using theRefitSettings.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 theUrlParameterKeyFormatter property. Remember that if a property has anAliasAs attribute, it will be used regardless of the formatter.

    The following example uses the built-inCamelCaseUrlParameterKeyFormatter:

    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.

Formatting URL Parameter Values with theUrlParameterFormatter

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.

Body content

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 isStream, the content will be streamed viaStreamContent
  • If the type isstring, 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).

Buffering and theContent-Length header

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 content

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;}}
JSON source generator

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 Content

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}}})});

Form posts

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")].

Setting request headers

Static headers

You can set one or more static request headers for a request applying aHeadersattribute 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);}

Dynamic headers

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);

Bearer Authentication

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

  1. Add[Headers("Authorization: Bearer")] to the interface or methods which need the token.
  2. SetAuthorizationHeaderValueGetter 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.

Reducing header boilerplate with DelegatingHandlers (Authorization headers worked example)

Although we make provisions for adding dynamic headers at runtime directly in Refit,most use-cases would likely benefit from registering a 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

Redefining headers

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 method
  • Header 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);

Removing headers

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,"");

Passing state into DelegatingHandlers

If there is runtime state that you need to pass to aDelegatingHandler you can add a property with a dynamic value to the underlyingHttpRequestMessage.Propertiesby 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.

Support for Polly and Polly.Context

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 contextas 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

Target Interface Type and method info

There may be times when you want to know what the target interface type is of the Refit instance. An example is where 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.

Multipart uploads

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.

Retrieving the response

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;

Using generic interfaces

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");

Interface inheritance

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.

Headers inheritance

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.

Default Interface Methods

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}";}

Using HttpClientFactory

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);}}

Providing a custom HttpClient

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;

Handling exceptions

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>>.

When returningTask<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.

When returningTask<T>

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}// ...

Providing a customExceptionFactory

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.

Providing a customDeserializationExceptionFactory

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;});

ApiException deconstruction with Serilog

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.

Topics

Resources

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
COPYING

Code of conduct

Stars

Watchers

Forks

Sponsor this project

 

Languages


[8]ページ先頭

©2009-2025 Movatter.jp