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

Easy-to-use typesafe REST API client library for .NET Standard 1.1 and .NET Framework 4.5 and higher, which is simple and customisable. Inspired by Refit

License

NotificationsYou must be signed in to change notification settings

canton7/RestEase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project Icon RestEase

NuGetBuild status

RestEase is a little type-safe REST API client library for .NET Framework 4.5.2 and higher and .NET Platform Standard 1.1 and higher, which aims to make interacting with remote REST endpoints easy, without adding unnecessary complexity.

To use it, you define an interface which represents the endpoint you wish to communicate with (more on that in a bit), where methods on that interface correspond to requests that can be made on it. RestEase will then generate an implementation of that interface for you, and by calling the methods you defined, the appropriate requests will be made. SeeInstallation andQuick Start to get up and running!

Almost every aspect of RestEase can be overridden and customized, leading to a large level of flexibility.

It also works on platforms which don't support runtime code generation, such as .NET Native and iOS, if you referenceRestEase.SourceGenerator. SeeUsing RestEase.SourceGenerator for more information.

RestEase is built on top ofHttpClient and is deliberately a "leaky abstraction": it is easy to gain access to the full capabilities of HttpClient, giving you control and flexibility, when you need it.

RestEase is inspired byAnaïs Betts' Refit, which in turn is inspired by Retrofit.

Table of Contents

  1. Installation
  2. Quick Start
  3. Request Types
  4. Return Types
  5. Query Parameters
    1. Constant Query Parameters
    2. Variable Query Parameters
      1. Formatting Variable Query Parameters
      2. Serialization of Variable Query Parameters
    3. Query Parameters Map
    4. Raw Query String Parameters
    5. Query Properties
  6. Paths
    1. Base Address
    2. Base Path
    3. Path Placeholders
      1. Path Parameters
        1. Formatting Path Parameters
        2. URL Encoding in Path Parameters
        3. Serialization of Path Parameters
      2. Path Properties
        1. Formatting Path Properties
        2. URL Encoding in Path Properties
        3. Serialization of Path Properties
  7. Body Content
    1. URL Encoded Bodies
  8. Response Status Codes
  9. Cancelling Requests
  10. Headers
    1. Constant Interface Headers
    2. Variable Interface Headers
      1. Formatting Variable Interface Headers
    3. Constant Method Headers
    4. Variable Method Headers
      1. Formatting Variable Method Headers
    5. Redefining Headers
  11. Using RestEase.SourceGenerator
  12. Using HttpClientFactory
  13. Using RestEase with Polly
    1. Using Polly withRestClient
    2. Using Polly with HttpClientFactory
  14. HttpClient and RestEase interface lifetimes
  15. Controlling Serialization and Deserialization
    1. CustomJsonSerializerSettings
    2. Custom Serializers and Deserializers
      1. Deserializing responses:ResponseDeserializer
      2. Serializing request bodies:RequestBodySerializer
      3. Serializing request query parameters:RequestQueryParamSerializer
      4. Serializing request path parameters:RequestPathParamSerializer
      5. Controlling query string generation:QueryStringBuilder
  16. Controlling the Requests
    1. RequestModifier
    2. CustomHttpClient
    3. Adding toHttpRequestMessage.Properties
  17. Customizing RestEase
  18. Interface Accessibility
  19. Using Generic Interfaces
  20. Using Generic Methods
  21. Interface Inheritance
    1. Sharing common properties and methods
    2. IDisposable
  22. Advanced Functionality Using Extension Methods
    1. Wrapping Other Methods
    2. UsingIRequester Directly
  23. FAQs

Installation

RestEase is available on NuGet. See that page for installation instructions.

If you're using C# 9 or .NET 5 (or higher), referenceRestEase.SourceGenerator as well to get compile-time errors and faster execution. SeeUsing RestEase.SourceGenerator for more information. If you're targetting iOS or .NET Native, you will need to do this, as runtime code generation isn't available.

If you're using ASP.NET Core, take a look atUsing HttpClientFactory. For failure handling and retries using Polly, seeUsing RestEase with Polly.

Quick Start

To start, first create an public interface which represents the endpoint you wish to make requests to. Please note that it does have to be public, or you must add RestEase as a friend assembly, seeInterface Accessibility below.

usingSystem;usingSystem.Threading.Tasks;usingNewtonsoft.Json;usingRestEase;namespaceRestEaseSampleApplication{// We receive a JSON response, so define a class to deserialize the json intopublicclassUser{publicstringName{get;set;}publicstringBlog{get;set;}// This is deserialized using Json.NET, so use attributes as necessary[JsonProperty("created_at")]publicDateTimeCreatedAt{get;set;}}// Define an interface representing the API// GitHub requires a User-Agent header, so specify one[Header("User-Agent","RestEase")]publicinterfaceIGitHubApi{// The [Get] attribute marks this method as a GET request// The "users" is a relative path the a base URL, which we'll provide later// "{userId}" is a placeholder in the URL: the value from the "userId" method parameter is used[Get("users/{userId}")]Task<User>GetUserAsync([Path]stringuserId);}publicclassProgram{publicstaticvoidMain(string[]args){// Create an implementation of that interface// We'll pass in the base URL for the APIIGitHubApiapi=RestClient.For<IGitHubApi>("https://api.github.com");// Now we can simply call methods on it// Normally you'd await the request, but this is a console appUseruser=api.GetUserAsync("canton7").Result;Console.WriteLine($"Name:{user.Name}. Blog:{user.Blog}. CreatedAt:{user.CreatedAt}");Console.ReadLine();}}}

Request Types

See the[Get("path")] attribute used above? That's how you mark that method as being a GET request. There are a number of other attributes you can use here - in fact, there's one for each type of request:[Get("path")],[Post("path")],[Put("path")],[Delete("path")],[Head("path")],[Options("path")],[Trace("path"))],[Patch("path")]. Use whichever one you need to.

The argument to[Get] (or[Post], or whatever) is typically a relative path, and will be relative to the base uri that you provide toRestClient.For<T>. (Youcan specify an absolute path here if you need to, in which case the base uri will be ignored). Also see the section onPaths.

Return Types

Your interface methods may return one of the following types:

  • Task: This method does not return any data, but the task will complete when the request has completed
  • Task<T> (whereT is not one of the types listed below): This method will deserialize the response into an object of typeT, using Json.NET (or a custom deserializer, seeControlling Serialization and Deserialization below).
  • Task<string>: This method returns the raw response, as a string (although this can be customised,see here).
  • Task<HttpResponseMessage>: This method returns the rawHttpResponseMessage resulting from the request. It does not do any deserialiation. You must dispose this object after use.
  • Task<Response<T>>: This method returns aResponse<T>. AResponse<T> contains both the deserialied response (of typeT), but also theHttpResponseMessage. Use this when you want to have both the deserialized response, and access to things like the response headers. You must dispose this object after use.
  • Task<Stream>: This method returns a Stream containing the response. Use this to e.g. download a file and stream it to disk. You must dispose this object after use.

Non-async methods are not supported (use.Wait() or.Result as appropriate if you do want to make your request synchronous).

If you return aTask<HttpResponseMessage> or aTask<Stream>, thenHttpCompletionOption.ResponseHeadersRead is used, so that you can choose whether or not the response body should be fetched (or report its download progress, etc). If however you return aTask<T>,Task<string>, orTask<Response<T>>, thenHttpCompletionOption.ResponseContentRead is used, meaning that anyCancellationToken that you pass will cancel the body download.If you return aTask, then the response body isn't fetched, unless anApiException is thrown.

Query Parameters

It is very common to want to include query parameters in your request (e.g./foo?key=value), and RestEase makes this easy.

Constant Query Parameters

The most basic type of query parameter is a constant - the value never changes. For these, simply put the query parameter as part of the URL:

publicinterfaceIGitHubApi{[Get("users/list?sort=desc")]Task<List<User>>GetUsersAsync();}

Variable Query Parameters

Any parameters to a method which are:

  • Decorated with the[Query] attribute, or
  • Not decorated at all

will be interpreted as query parameters.

The name of the parameter will be used as the key, unless an argument is passed to[Query("key")], in which case that will be used instead.

For example:

publicinterfaceIGitHubApi{[Get("user")]Task<User>FetchUserAsync(intuserid);// Is the same as:[Get("user")]Task<User>FetchUserAsync([Query]intuserid);// Is the same as:// (Note the casing of the parameter name)[Get("user")]Task<User>FetchUserAsync([Query("userid")]intuserId);}IGithubApiapi=RestClient.For<IGithubApi>("http://api.github.com");// Requests http://api.github.com/user?userid=3awaitapi.FetchUserAsync(3);

You can have duplicate keys if you want:

publicinterfaceISomeApi{[Get("search")]Task<SearchResult>SearchAsync([Query("filter")]stringfilter1,[Query("filter")]stringfilter2);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests http://somenedpoint.com/search?filter=foo&filter=barawaitapi.SearchAsync("foo","bar");

You can also have an array of query parameters:

publicinterfaceISomeApi{// You can use IEnumerable<T>, or any type which implements IEnumerable<T>[Get("search")]Task<SearchResult>SearchAsync([Query("filter")]IEnumerable<string>filters);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests http://api.exapmle.com/search?filter=foo&filter=bar&filter=bazawaitapi.SearchAsync(new[]{"foo","bar","baz"});

If you specify a key that isnull, i.e.[Query(null)], then the name of the key is not used, and the value is inserted into the query string. If you specify a key that is an empty string"", then then query key will be left empty.

publicinterfaceISomeApi{[Get("foo")]TaskFooAsync([Query(null)]stringnullParam,[Query("")]stringemptyParam);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/foo?onitsown&=nokeyawaitapi.FooAsync("onitsown","nokey");

If you pass a value which is null, then the key is not inserted. If you pass any other value (e.g. emptystring) then the value is left empty.

publicinterfaceISomeApi{[Get("path")]TaskFooAsync([Query]stringfoo,[Query]stringbar);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/path?bar=awaitapi.FooAsync(null,"");

Formatting Variable Query Parameters

By default, query parameter values will be serialized by callingToString() on them. This means that the primitive types most often used as query parameters -string,int, etc - are serialized correctly.

However, you can also specify a string format to use using theFormat property of the[Query] attribute, for example:

publicinterfaceISomeApi{[Get("foo")]TaskFooAsync([Query(Format="X2")]intparam);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/foo?param=FEawaitapi.FooAsync(254);
  1. If you use acustom serializer, then the format is passed to that serializer, and you can use it as you like.
  2. Otherwise, if the format looks like it could be passed tostring.Format, then this happens withparam passed as the first arg, andRestClient.FormatProvider as theIFormatProvider. For example,"{0}" or"{0:X2}" or"hello {0}".
  3. Otherwise, ifparam implementsIFormattable, then itsToString(string, IFormatProvider) method is called, withparam as the format andRestClient.FormatProvider as theIFormatProvider. For example,"X2".
  4. Otherwise, the format is ignored.

Serialization of Variable Query Parameters

Sometimes callingToString() is not enough: some APIs require that you send e.g. JSON as a query parameter. In this case, you can mark the parameter for custom serialization usingQuerySerializationMethod.Serialized, and further control it by using acustom serializer.

For example:

publicclassSearchParams{publicstringTerm{get;set;}publicstringMode{get;set;}}publicinterfaceISomeApi{[Get("search")]Task<SearchResult>SearchAsync([Query(QuerySerializationMethod.Serialized)]SearchParamsparam);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/search?params={"Term": "foo", "Mode": "basic"}awaitapi.SearchAsync(newSearchParams(){Term="foo",Mode="basic"});

You can also specify the default serialization method for an entire api by specifying[SerializationMethods(Query = QuerySerializationMethod.Serialized)] on the interface, or for all parameters in a given method by specifying it on the method, for example:

[SerializationMethods(Query=QuerySerializationMethod.Serialized)]publicinterfaceISomeApi{[Get("search")][SerializationMethods(Query=QuerySerializationMethod.ToString)]Task<SearchResult>SearchWithToStringAsync([Query]SearchParamsparam);[Get("search")]Task<SearchResult>SearchWithSerializedAsync([Query]SearchParamsparam);}

Query Parameters Map

Sometimes you have a load of query parameters, or they're generated dynamically, etc. In this case, you may want to supply a dictionary of query parameters, rather than specifying a load of method parameters.

To facilitate this, you may decorate one or more method parameters with[QueryMap]. The parameter type must be anIDictionary<TKey, TValue>.

Query maps are handled the same way as other query parameters: serialization, handling of enumerables, null values, etc, behave the same. You can control whether values are serialized using a custom serializer orToString() using e.g.[QueryMap(QuerySerializationMethod.Serialized)].

For example:

publicinterfaceISomeApi{[Get("search")]// I've used IDictionary<string, string[]> here, but you can use whatever type parameters you like,// or any type which implements IDictionary<TKey, TValue>Task<SearchResult>SearchBlogPostsAsync([QueryMap]IDictionary<string,string[]>filters);}varapi=RestClient.For<ISomeApi>("https://api.example.com");varfilters=newDictionary<string,string[]>(){{"title",new[]{"bobby"}},{"tag",new[]{"c#","programming"}}};// Requests https://api.example.com/search?title=bobby&tag=c%23&tag=programmingvarsearchResults=awaitapi.SearchBlogPostsAsync(filters);

Raw Query String Parameters

In rare cases, you may have generated a query string by other means, and want to give this to RestEase. To do this, provide one or more parameters decorated with[RawQueryString].

This parameter can be of any type, and.ToString() will be called on it to turn it into a string. Its value will be prepended, verbatim, to the query string: you are responsible for any escaping. It must not begin or end with "&" or "?".

For example:

publicinterfaceISomeApi{[Get("search")]Task<SearchResult>SearchAsync([RawQueryString]stringcustomFilter);}varapi=RestClient.For<ISomeApi>("https://api.example.com");varfilter="filter=foo";// Requests https://api.example.com?filter=foovarsearchResults=awaitapi.SearchAsync(filter);

Query Properties

If you want to have a query string which is included in all of your requests, you can do this by declaring a[Query] property. These work the same way as query parameters, but they apply to all methods in your interface. If the value of the property isnull, then it will not be added to your query. Otherwise, it will always be present, even if you declare a query parameter with the same name.

The property must have both a getter and a setter.

For example:

publicinterfaceISomeApi{[Query("foo")]stringFoo{get;set;}[Get("thing")]TaskThingAsync([Query]stringfoo);}varapi=RestClient.For<ISomeApi>("https://api.example.com");api.Foo="bar";// Requests https://api.example.com?foo=baz&foo=barawaitapi.ThingAsync("baz");

Paths

The path to which a request is sent is constructed from the following 3 parts, concatenated together (and separated with/):

  1. The Base Address (e.g.https://api.example.com)
  2. The Base Path (optional, e.g.api/v1)
  3. The path specified on the method, passed to the[Get("path")], etc, attribute (e.g.users)

The Base Address is the domain at which the API can be found. This is normally specified by passing an address toRestClient.For<T>(...), but you can also use[BaseAddress(...)], seeBase Address.

The Base Path is optional, and is a prefix which is common to all of your API's paths, e.g.api/v1. If you like, you can specify this once using the[BasePath(...)] attribute, seeBase Path.

Each method also has a path, which is passed to the[Get(...)], etc, attribute on the method.

Ordinarily, these three parts are concatenated together, giving e.g.https://api.example.com/api/v1/users. However, there are a number of extra rules:

  1. If the path specified on the method begins with a/, then the Base Path is ignored (but the Base Address is not ignored). So if the method's path was/users instead ofusers, the final address would behttps://api.example.com/users.
  2. If the path specified on the method is absolute (e.g. it begins withhttp://), then both the Base Address and Base Path are ignored.
  3. If the Base Path is absolute, then the Base Address is ignored.

These rules are particularly useful when working with an API which returns links to other endpoints, seeURL Encoding in Path Parameters for an example.

Base Address

The Base Address can be specified in two ways:

  1. When instantiating the API usingRestClient, either by passing a URI toRestClient.For<T>(...) ornew RestClient(...), or by passing aHttpClient which has itsBaseAddress property set.
  2. Using a[BaseAddress(...)] attribute on the interface itself.

If it's specified both ways, then the[BaseAddress(...)] attribute is ignored. This means that you can have a default Base Address specified on the interface, but this can be overridden when actually instantiating it usingRestClient.

The Base Address can contain{placeholders}. Each placeholder must have a correspondingpath property, although this will be overridden by apath parameter if one is present.

The Base Address may contain the start of a path as well, e.g.https://api.example.com/api/v1. This path will not be overridden if the path specified on the method starts with a/, in contrast to the Base Path.

The Base Address must be absolute (i.e. it must start withhttp:// orhttps://).

Query strings or fragments are not supported in the Base Address, and their behaviour is undefined and subject to change.

Base Path

The Base Path is specified using a[BasePath(...)] attribute on your interface.

The Base Path can contain{placeholders}. Each placeholder must have a correspondingpath property, although this will be overridden by apath parameter if one is present.

Query strings, or other parts of a URI, are not supported in the Base Path, and their behaviour is undefined and subject to change.

Path Placeholders

Sometimes you also want to be able to control some parts of the path itself, rather than just the query parameters.

Path Parameters

Path parameters are the most common means of controlling a part of the path. This is done using placeholders in the path, and corresponding method parameters decorated with[Path].

For example:

publicinterfaceISomeApi{[Get("user/{userId}")]Task<User>FetchUserAsync([Path]stringuserId);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/user/fredawaitapi.FetchUserAsync("fred");

As with[Query], the name of the placeholder to substitute is determined by the name of the parameter. If you want to override this, you can pass an argument to[Path("placeholder")], e.g.:

publicinterfaceISomeApi{[Get("user/{userId}")]Task<User>FetchUserAsync([Path("userId")]stringidOfTheUser);}

Every placeholder must have a corresponding parameter, and every parameter must relate to a placeholder.

Formatting Path Parameters

As with[Query], path parameter values will be serialized by callingToString() on them. This means that the primitive types most often used as query parameters -string,int, etc - are serialized correctly.

However, you can also specify a format using theFormat property of the[Path] attribute, for example:

publicinterfaceISomeApi{[Get("foo/{bar}")]TaskFooAsync([Path("bar",Format="D2")]intparam);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/foo/01awaitapi.FooAsync(1);
  1. If you use acustom serializer, then the format is passed to that serializer, and you can use it as you like.
  2. Otherwise, if the format looks like it could be passed tostring.Format, then this happens withparam passed as the first arg, andRestClient.FormatProvider as theIFormatProvider. For example,"{0}" or"{0:D2}" or"hello {0}".
  3. Otherwise, ifparam implementsIFormattable, then itsToString(string, IFormatProvider) method is called, withparam as the format andRestClient.FormatProvider as theIFormatProvider. For example,"D2".
  4. Otherwise, the format is ignored.
URL Encoding in Path Parameters

By default, path parameters are URL-encoded, which means things like/ are escaped. If you don't want this, for example you want to specify a literal section of the URL, this can be disabled using theUrlEncode property of the[Path] attribute, for example:

publicinterfaceISomeApi{[Get("foo/{bar}")]TaskFooAsync([Path(UrlEncode=false)]stringbar);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/foo/bar/bazawaitapi.FooAsync("bar/baz");

This can be useful if working with an API which returns raw links to other resources, when combined with the logic specified inPaths. For example, let's say we want to make a request tohttps://api.example.com/v1/first, and that gives us back:

{"Second":"/v1/second"}

We could write:

[BasePath("v1")]publicinterfaceISomeApi{[Get("first")]Task<FirstResponse>GetFirstAsync();[Get("{url}")]TaskGetSecondAsync([Path]stringurl);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");varresponse=awaitapi.GetFirstAsync();awaitapi.GetSecondAsync(response.Second);
Serialization of Path Parameters

Similar to query parameters, callingToString() is sometimes not enough: you might want to customize how your path parameters are turned into strings (for example, for enum members). In this case, you can mark the parameter for custom serialization usingPathSerializationMethod.Serialized, and specifying aRequestPathParamSerializer.

For example:

publicenumMyEnum{[Display(Name="first")]First,[Display(Name="second")]Second,}publicinterfaceISomeApi{[Get("path/{param}")]Task<string>GetAsync([Path(PathSerializationMethod.Serialized)]MyEnumparam);}ISomeApiapi=newRestClient(){RequestPathParamSerializer=newStringEnumRequestPathParamSerializer()}.For<ISomeApi>("https://api.example.com");// Requests https://api.example.com/path/firstawaitapi.GetAsync(MyEnum.First);

You can also specify the default serialization method for an entire api by specifying[SerializationMethods(Path = PathSerializationMethod.Serialized)] on the interface, or for all parameters in a given method by specifying it on the method, for example:

[SerializationMethods(Path=PathSerializationMethod.Serialized)]publicinterfaceISomeApi{[Get("path/{foo}")][SerializationMethods(Path=PathSerializationMethod.ToString)]Task<string>GetWithToStringAsync([Path]MyEnumfoo);[Get("path/{foo}")]Task<string>GetWithSerializedAsync([Path]MyEnumfoo);}

Path Properties

Sometimes you've got a placeholder which is present in all (or most) of the paths on the interface, for example an account ID. In this case, you can specify a[Path] property. These work in the same way as path parameters, but they're on the level of the entire API.

Properties must have both a getter and a setter. If the placeholder of the path property isn't given (i.e. you use[Path] instead of[Path("placeholder")]), then the name of the property will be used.

Unlike with path parameters, you don'tneed to have the placeholder present in every path. If you have both a path parameter and a path property with the same name, the path parameter is used.

For example:

publicinterfaceISomeApi{[Path("accountId")]intAccountId{get;set;}[Get("{accountId}/profile")]Task<Profile>GetProfileAsync();[Delete("{accountId}")]TaskDeleteAsync([Path("accountId")]intaccountId);}varapi=RestClient.For<ISomeApi>("https://api.example.com/user");api.AccountId=3;// Requests https://api.example.com/user/3/profilevarprofile=awaitapi.GetProfileAsync();// Requests https://api.example.com/user/4/profileawaitapi.DeleteAsync(4);

You can also useBasePath if all of your paths start with{accountId}.

Formatting Path Properties

As with Path Parameters, you can specify a string format to use if the value implementsIFormattable.

For example:

publicinterfaceISomeApi{[Path("accountId",Format="N")]GuidAccountId{get;set;}[Get("{accountId}/profile")]Task<Profile>GetProfileAsync();}varapi=RestClient.For<ISomeApi>("https://api.example.com/user");api.AccountId=someGuid;// Requests e.g. /user/00000000000000000000000000000000 /profilevarprofile=awaitapi.GetProfileAsync();
URL Encoding in Path Properties

As with path parameters, you can disable URL encoding for path properties.

For example:

publicinterfaceISomeApi{[Path("pathPart",UrlEncode=false)]GuidPathPart{get;set;}[Get("{pathPart}/profile")]TaskGetAsync();}varapi=RestClient.For<ISomeApi>("https://api.example.com");api.PathPart="users/abc";// Requests https://api.example.com/users/abc/profileawaitapi.GetAsync();
Serialization of Path Properties

As with path parameters, you can specifyPathSerializationMethod.Serialized on a path property to use custom serialization behaviour. You must also supply aRequestPathParamSerializer when creating theRestClient. This can be used for things like controlling how enum members are serialized.

For example:

publicenumMyEnum{[Display(Name="first")]First,[Display(Name="second")]Second,}publicinterfaceISomeApi{[Path("pathPart",PathSerializationMethod.Serialized)]MyEnumPathPart{get;set;}[Get("path/{pathPart}")]Task<string>GetAsync();}ISomeApiapi=newRestClient(){RequestPathParamSerializer=newStringEnumRequestPathParamSerializer()}.For<ISomeApi>("https://api.example.com");api.PathPart=MyEnumSecond;// Requests https://api.example.com/path/secondawaitapi.GetAsync();

Body Content

If you're sending a request with a body, you can specify that one of the parameters to your method contains the body you want to send, using the[Body] attribute.

publicinterfaceISomeApi{[Post("users/new")]TaskCreateUserAsync([Body]Useruser);}

Exactly how this will be serialized depends on the type of parameters:

  • If the type isStream, then the content will be streamed viaStreamContent.
  • If the type isString, then the string will be used directly as the content (usingStringContent).
  • If the type isbyte[], then the byte array will be used directory as the content (usingByteArrayContent).
  • If the parameter has the attribute[Body(BodySerializationMethod.UrlEncoded)], then the content will be URL-encoded (see below).
  • If the type is anHttpContent (or one of its subclasses), then it will be used directly. This is useful for advanced scenarios.
  • Otherwise, the parameter will be serialized as JSON (by default, or you can customize this if you want, seeControlling Serialization and Deserialization).

URL Encoded Bodies

For APIs which take form posts (i.e. serialized asapplication/x-www-form-urlencoded), initialize the[Body] attribute withBodySerializationMethod.UrlEncoded. This parameter must implementIDictionary orIDictionary<TKey, TValue>.

If any of the values implementIEnumerable, then they will be serialized as an array of values.

For example:

publicinterfaceIMeasurementProtocolApi{[Post("collect")]TaskCollectAsync([Body(BodySerializationMethod.UrlEncoded)]Dictionary<string,object>data);}vardata=newDictionary<string,object>{{"v",1},{"tids",new[]{"UA-1234-5","UA-1234-6"},{"cid",newGuid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")},{"t","event"},};// Serialized as: v=1&tids=UA-1234-5&tids=UA-1234-6&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=eventawaitapi.CollectAsync(data);

You can also control the default body serialization method for an entire API by specifying[SerializationMethods(Body = BodySerializationMethod.UrlEncoded)] on the interface itself:

[SerializationMethods(Body=BodySerializationMethod.UrlEncoded)]publicinterfaceISomeApi{[Post("collect")]TaskCollectAsync([Body]Dictionary<string,object>data);}

Response Status Codes

By default, any response status code which does not indicate success (as indicated byHttpResponseMessage.IsSuccessStatusCode) will cause anApiException to be thrown.

TheApiException has properties which tell you exactly what happened (such as theHttpStatusCode, the URI which was requested, the string content, and also a methodDeserializeContent<T>() to let you attempt to deserialize the content as a particular type). This means that you can write code such as:

try{varresponse=awaitclient.SayHelloAsync();}catch(ApiExceptione)when(e.StatusCode==HttpStatusCode.NotFound){varnotFoundResponse=e.DeserializeContent<NotFoundRepsonse>();// ...}

This is usually what you want, but sometimes you're expecting failure.

In this case, you can apply[AllowAnyStatusCode] to you method, or indeed to the whole interface, to suppress this behaviour. If you do this, then you probably want to make your method return either aHttpResponseMessage or aResponse<T> (seeReturn Types) so you can examine the response code yourself.

For example:

publicinterfaceISomeApi{[Get("users/{userId}")][AllowAnyStatusCode]Task<Response<User>>FetchUserThatMayNotExistAsync([Path]intuserId);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");using(varresponse=awaitapi.FetchUserThatMayNotExistAsync(3));if(response.ResponseMessage.StatusCode==HttpStatusCode.NotFound){// User wasn't found}else{varuser=response.GetContent();// ...}

Cancelling Requests

If you want to be able to cancel a request, pass aCancellationToken as one of the method paramters.

publicinterfaceISomeApi{[Get("very-large-response")]Task<LargeResponse>GetVeryLargeResponseAsync(CancellationTokencancellationToken);}

Note that if your method returns aTask<HttpResponseMessage> orTask<Stream>, then theCancellationToken will not cancel the download of the response body, seeReturn Types for details.

Headers

Specifying headers is actually a surprisingly large topic, and can be done in several ways, depending on the precise behaviour you want.

Constant Interface Headers

If you want to have a header that applies to every single request, and whose value is fixed, use a constant interface header. These are specified as[Header("Name", "Value")] attributes on the interface.

For example:

[Header("User-Agent","RestEase")][Header("Cache-Control","no-cache")]publicinterfaceIGitHubApi{[Get("users")]Task<List<User>>GetUsersAsync();}

Variable Interface Headers

If you want to have a header that applies to every single request, and whose value is variable, then use a variable interface header. These are specifed using properties, using a[Header("Name")] attribute on that property.

For example:

publicinterfaceISomeApi{[Header("X-API-Key")]stringApiKey{get;set;}[Get("users/{userId}")]Task<User>FetchUserId([Path]stringuserId);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com")api.ApiKey="The-API-KEY-value";// ...

For nullable property types, you can also specify a default (which will be used when the property is null):

publicinterfaceISomeApi{[Header("X-API-Key","None")]stringApiKey{get;set;}[Get("users/{userId}")]Task<User>FetchUserAsync([Path]stringuserId);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com")// "X-API-Key: None"varuser=awaitapi.FetchUserAsync("bob");

Formatting Variable Interface Headers

By default, variable interface header values will be serialized by callingToString() on them. This means that the primitive types most often used as query parameters -string,int, etc - are serialized correctly.

However, you can also specify a string format to use using theFormat property of the[Query] attribute, for example:

publicinterfaceISomeApi{[Header("SomeHeader",Format="X2")]intSomeHeader{get;set;}[Get("foo")]TaskFooAsync();}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");api.SomeHeader=254;// SomeHeader: FEawaitapi.FooAsync();
  1. If the format looks like it could be passed tostring.Format, then this happens withSomeHeader passed as the first arg, andRestClient.FormatProvider as theIFormatProvider. For example,"{0}" or"{0:X2}" or"hello {0}".
  2. Otherwise, ifSomeHeader implementsIFormattable, then itsToString(string, IFormatProvider) method is called, withSomeHeader as the format andRestClient.FormatProvider as theIFormatProvider. For example,"X2".
  3. Otherwise, the format is ignored.

Constant Method Headers

If you want to have a header which only applies to a particular method, and whose value never changes, then use a constant method header. Like constant interface headers, these are defined in their entirety using an attribute. However, instead of applying the attribute to the interface, you apply it to the method.

publicinterfaceIGitHubApi{[Header("User-Agent","RestEase")][Header("Cache-Control","no-cache")][Get("users")]Task<List<User>>GetUsersAsync();// This method doesn't have any headers applied[Get("users/{userId}")]Task<User>GetUserAsync([Path]stringuserId);}

Variable Method Headers

Finally, you can have headers which only apply to a single method and whose values are variable. These consist of a[Header("Name")] attribute applied to a method parameter.

publicinterfaceISomeApi{[Get("users")]Task<List<User>>GetUsersAsync([Header("Authorization")]stringauthorization);}

Formatting Variable Method Headers

By default, variable method header values will be serialized by callingToString() on them. This means that the primitive types most often used as query parameters -string,int, etc - are serialized correctly.

However, you can also specify a string format to use using theFormat property of the[Query] attribute, for example:

publicinterfaceISomeApi{[Get("foo")]TaskFooAsync([Header("SomeHeader",Format="X2")]intsomeHeader);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// SomeHeader: FEawaitapi.FooAsync(254);
  1. If the format looks like it could be passed tostring.Format, then this happens withsomeHeader passed as the first arg, andRestClient.FormatProvider as theIFormatProvider. For example,"{0}" or"{0:X2}" or"hello {0}".
  2. Otherwise, ifsomeHeader implementsIFormattable, then itsToString(string, IFormatProvider) method is called, withsomeHeader as the format andRestClient.FormatProvider as theIFormatProvider. For example,"X2".
  3. Otherwise, the format is ignored.

Redefining Headers

You've probably noticed that there are 4 places you can define a header: on the interface, as a property, on a method, and as a parameter (or, Constant Interface Headers, Variable Interface Headers, Constant Method Headers, and Variable Method Headers, respectively). There are rules specifying how headers from different places are merged.

Constant and Variable Interface headers are merged, as are Constant and Variable Method headers. That is, if a header is supplied both as an attribute on the interface, and as a property, that header will have multiple values.

Method headers will replace Interface headers. If you have the same header on a method and on the interface, then the header on the method will replace the one on the interface.

Another rule is that a header with a value ofnull will not be added, but can still replace a previously-defined header of the same name.

Example time:

[Header("X-InterfaceOnly","InterfaceValue")][Header("X-InterfaceAndParamater","InterfaceValue")][Header("X-InterfaceAndMethod","InterfaceValue"][Header("X-InterfaceAndParameter","InterfaceValue"][Header("X-InterfaceAndMethod-ToBeRemoved","InterfaceValue")]publicinterface ISomeApi{[Header("X-ParameterOnly")]    string ParameterOnlyHeader{get;set;}[Header("X-InterfaceAndParameter")]    string InterfaceAndParameterHeader{get;set;}[Header("X-ParameterAndMethod")]    string ParameterAndMethodHeader{get;set;}[Get("url")][Header("X-MethodOnly","MethodValue")][Header("X-MethodAndParameter","MethodValue")][Header("X-ParameterAndMethod","MethodValue")][Header("X-InterfaceAndMethod-ToBeRemoved",null)]    Task DoSomethingAsync([Header("X-ParameterOnly")]string parameterOnly,[Header("X-MethodAndParameter")]string methodAndParameter,[Header("X-InterfaceAndParameter")]string interfaceAndParameter);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");api.ParameterOnlyHeader="ParameterValue";api.InterfaceAndParameterHeader="ParameterValue";api.ParameterAndMethodHeader="ParameterValue";awaitapi.DoSomethingAsync("ParameterValue","ParameterValue","ParameterValue");// Has the following headers:// X-InterfaceOnly: InterfaceValue// X-InterfaceAndParameter: InterfaceValue, ParameterValue// X-InterfaceAndMethod: MethodValue// X-InterfaceAndParameter: ParameterValue// X-ParameterAndMethod: MethodValue// X-MethodOnly: MethodValue// X-MethodAndParameter: MethodValue, ParameterValue// X-ParameterAndMethod-ToBeRemoved isn't set, because it was removed

Using RestEase.SourceGenerator

Source Generators are a new feature which allows NuGet packages to hook into the compilation of your projects and insert their own code. RestEase uses this to generate implementations of your interfaces at compile-time, rather than run-time. To take advantage of this, you need to install theRestEase.SourceGenerator NuGet package as well as RestEase.

The advantages of using a Source Generator are:

  1. Compile-time error checking. Find out if your RestEase interface has an error at compile-time, rather than runtime.
  2. Supports platforms which don't support System.Reflection.Emit, such as iOS and .NET Native.
  3. Faster: no need to generate implementations at runtime.

You will need to be using the .NET 5 SDK (or higher) to make use of source generators. If you're targetting C# 9 or .NET 5 (or higher), you're all set. If you're targetting an earlier language or runtime version, you can still install the latest .NET SDK (make sure you update your global.json if you have one!): you don't need to be targetting .NET 5, you just need to be building with the .NET 5 SDK.

When you build a project which references RestEase.SourceGenerator, RestEase generates implementations of any RestEase interfaces it finds in that project, and adds those implementations to your project.RestClient.For<T> will look for one of these implementations first, before falling back to the old approach of generating one at runtime. This means that you should reference RestEase.SourceGenerator in all projects which contain RestEase interfaces, but projects which only consume interfaces can just reference the RestEase package.

Authors of libraries which expose RestEase interfaces should also install RestEase.SourceGenerator, and the consumers of your library will use the interface implementations it generates a compile-time without needing to install RestEase.SourceGenerator themselves.

Using HttpClientFactory

If you're using ASP.NET Core 2.1 or higher, you can set up RestEase to use HttpClientFactory. Add a reference toRestEase.HttpClientFactory, and add something similar to the following to yourConfigureServices method:

services.AddRestEaseClient<ISomeApi>("https://api.example.com");

If you want to configure theRestClient instance, for example to set a custom serializer, pass in anoptions withRestClientConfigurer set to anAction<RestClient>:

services.AddRestEaseClient<ISomeApi>("https://api.example.com",new(){RestClientConfigurer= client=>client.RequestPathParamSerializer=newStringEnumRequestPathParamSerializer(),});

If you want fo configure theISomeApi instance, for example to assign a value to a property:

services.AddRestEaseClient<ISomeApi>("https://api.example.com",new(){InstanceConfigurer= instance=>instance.SomeHeader="Some Value",});

AnIHttpClientBuilder is returned, which you can call further methods on if needed:

services.AddRestEaseClient<ISomeApi>("https://api.example.com").AddHttpMessageHandler<SomeHandler>().SetHandlerLifetime(TimeSpan.FromMinutes(2));

You can then injectISomeApi into your controllers:

publicclassSomeController:ControllerBase{privatereadonlyISomeApisomeApi;publicSomeController(ISomeApisomeApi)=>this.someApi=someApi;}

If you want to configure aHttpClient for use with multiple RestEase interfaces, you can useIHttpClientBuilder.UseWithRestEaseClient. This returns theIHttpClientBuilder it was called on, so you can call it multiple times.

Make sure that you use one of theAddHttpClient overloads which takes a name, and also that you configure theBaseAddress on theHttpClient.

services.AddHttpClient("example").ConfigureHttpClient(x=>x.BaseAddress=newUri("https://api.example.com")).UseWithRestEaseClient<ISomeApi>().UseWithRestEaseClient<ISomeOtherApi>();

Using RestEase with Polly

Sometimes request fail, and you want to retry them.

Polly is the industry-standard framework for defining retry/failure/etc policies, and it's easy to integrate with RestEase.

Using Polly withRestClient

If you're working with RestEase usingnew RestClient(...).For<T>() orRestClient.For<T>(...), then you'll need to installMicrosoft.Extensions.Http.Polly. Create yourIAsyncPolicy<HttpResponseMessage> following the Polly documentation, and then tell RestEase to use it using aPolicyHttpMessageHandler:

// Define your policy however you wantvarpolicy=Policy.HandleResult<HttpResponseMessage>(r=>r.StatusCode==HttpStatusCode.NotFound).RetryAsync();// Tell RestEase to use itvarapi=RestClient.For<ISomeApi>("https://api.example.com",newPolicyHttpMessageHandler(policy));// ... or ...varapi=newRestClient("https://api.example.com",newPolicyHttpMessageHandler(policy)).For<ISomeApi>();

Using Polly with HttpClientFactory

If you're using HttpClientFactory, then you'll need to follow the instructions onusing HttpClientFactory, and then installMicrosoft.Extensions.Http.Polly.

You can then followthese Polly instructions, but useAddRestEaseClient instead ofAddHttpClient, for example:

// Define your policy however you wantvarpolicy=Policy.HandleResult<HttpResponseMessage>(r=>r.StatusCode==HttpStatusCode.NotFound).RetryAsync();services.AddRestEaseClient<ISomeApi>("https://api.example.com").AddPolicyHandler(policy);// ... or perhaps ...services.AddRestEaseClient<ISomeApi>("https://api.example.com").AddTransientHttpErrorPolicy(builder=>builder.WaitAndRetryAsync(new[]{TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(5),TimeSpan.FromSeconds(10)}));

HttpClient and RestEase interface lifetimes

Each instance of the interface which you define will create its own HttpClient instance.

Prior to .NET Core 2.1, you should avoid creating and destroying many HttpClient instances (e.g. one per client request in a web app): instead create a single instance and keep using it (see here).

When using RestEase, this means that you should create a single instance of your interface and reuse it. If you use properties (e.g. Path Properties or Header Properties) which are set more than once, you could create a singleton HttpClient, and pass it toRestClient.For<T> to create many instances of your interface which share the same HttpClient.

If you're using .NET Core 2.1+, don't worry:HttpClient works as expected.

Controlling Serialization and Deserialization

By default, RestEase will useJson.NET to deserialize responses, and serialize request bodies and query parameters. However, you can change this, either by specifying customJsonSerializerSettings, or by providing your own serializers / deserializers

CustomJsonSerializerSettings

If you want to specify your ownJsonSerializerSettings, you can do this by constructing a newRestClient, assigningJsonSerializerSettings, then callingFor<T>() to obtain an implementation of your interface, for example:

varsettings=newJsonSerializerSettings(){ContractResolver=newCamelCasePropertyNamesContractResolver(),Converters={newStringEnumConverter()}};varapi=newRestClient("https://api.example.com"){JsonSerializerSettings=settings}.For<ISomeApi>();

Custom Serializers and Deserializers

You can completely customize how requests are serialized, and responses deserialized, by providing your own serializer/deserializer implementations:

You can, of course, provide a custom implementation of only one of these, or all of them, or any number in between.

Deserializing responses:ResponseDeserializer

This class has a single method, which is called whenever a response is received which needs deserializing. It is passed theHttpResponseMessage (so you can read headers, etc, if you want) and itsstring content which has already been asynchronously read.

For an example, seeJsonResponseDeserializer.

To tell RestEase to use it, you must create a newRestClient, assign itsResponseDeserializer property, then callFor<T>() to get an implementation of your interface.

// This API returns XMLpublicclassXmlResponseDeserializer:ResponseDeserializer{publicoverrideTDeserialize<T>(stringcontent,HttpResponseMessageresponse,ResponseDeserializerInfoinfo){// Consider caching generated XmlSerializersvarserializer=newXmlSerializer(typeof(T));using(varstringReader=newStringReader(content)){return(T)serializer.Deserialize(stringReader);}}}// ...varapi=newRestClient("https://api.example.com"){ResponseDeserializer=newXmlResponseDeserializer()}.For<ISomeApi>();

If you want your deserializer to receive strings (which lets you change the default behaviour, where an interface method which returns aTask<string> will return the raw response body), set the propertyResponseDeserializer.HandlesStrings totrue.

Serializing request bodies:RequestBodySerializer

This class has a single method, which is called whenever a request body requires serialization (i.e. is decorated with[Body(BodySerializationMethod.Serialized)]). It returns anyHttpContent subclass you like, althoughStringContent is likely to be a common choice.

When writing anRequestBodySerializer'sSerializeBody implementation, you may choose to provide some default headers, such asContent-Type. These will be overidden by any[Header] attributes.

For an example, seeJsonRequestBodySerializer.

To tell RestEase to use it, you must create a newRestClient, assign itsRequestBodySerializer property, then callFor<T>() to get an implementation of your interface.

For example:

publicclassXmlRequestBodySerializer:RequestBodySerializer{publicoverrideHttpContentSerializeBody<T>(Tbody,RequestBodySerializerInfoinfo){if(body==null)returnnull;// Consider caching generated XmlSerializersvarserializer=newXmlSerializer(typeof(T));using(varstringWriter=newStringWriter()){serializer.Serialize(stringWriter,body);varcontent=newStringContent(stringWriter.ToString());// Set the default Content-Type header to application/xmlcontent.Headers.ContentType.MediaType="application/xml";returncontent;}}}// ...varapi=newRestClient("https://api.example.com"){RequestBodySerializer=newXmlRequestBodySerializer()}.For<ISomeApi>();

Serializing request query parameters:RequestQueryParamSerializer

This class has two methods: one is called whenever a scalar query parameter requires serialization (i.e. is decorated with[Query(QuerySerializationMethod.Serialized)]); the other is called whenever a collection of query parameters (that is, the query parameter has typeIEnumerable<T> for someT) requires serialization.

Both of these methods want you to return anIEnumerable<KeyValuePair<string, string>>, where each key corresponds to the name of a query name/value pair, and each value corresponds to the value.For example:

returnnew[]{newKeyValuePair<string,string>("foo","bar"),newKeyValuePair<string,string>("foo","baz"),newKeyValuePair<string,string>("yay","woo")}// Will get serialized to '...?foo=bar&foo=baz&yay=woo'

It is unlikely that you will return more than oneKeyValuePair from the method which serializes scalar query parameters, but the flexibility is there.

For an example, seeJsonRequestQueryParamSerializer.

To tell RestEase to use it, you must create a newRestClient, assign itsRequestQueryParamSerializer property, then callFor<T>() to get an implementation of your interface.

For example:

// It's highly unlikely that you'll get an API which requires xml-encoded query// parameters, but for the sake of an example:publicclassXmlRequestQueryParamSerializer:RequestQueryParamSerializer{publicoverrideIEnumerable<KeyValuePair<string,string>>SerializeQueryParam<T>(stringname,Tvalue,RequestQueryParamSerializerInfoinfo){if(value==null)yieldbreak;// Consider caching generated XmlSerializersvarserializer=newXmlSerializer(typeof(T));using(varstringWriter=newStringWriter()){serializer.Serialize(stringWriter,value);yieldreturnnewKeyValuePair<string,string>(name,stringWriter.ToString()));}}publicoverrideIEnumerable<KeyValuePair<string,string>>SerializeQueryCollectionParam<T>(stringname,IEnumerable<T>values,RequestQueryParamSerializerInfoinfo){if(values==null)yieldbreak;// Consider caching generated XmlSerializersvarserializer=newXmlSerializer(typeof(T));foreach(varvalueinvalues){if(value!=null){using(varstringWriter=newStringWriter()){serializer.Serialize(stringWriter,value);yieldreturnnewKeyValuePair<string,string>(name,stringWriter.ToString()));}}}}}varapi=newRestClient("https://api.example.com"){RequestQueryParamSerializer=newXmlRequestQueryParamSerializer()}.For<ISomeApi>();

If you specified aFormat property on the[Query] attribute, this will be available asinfo.Format. By default, this isnull.

Serializing request path parameters:RequestPathParamSerializer

This class has one method, called whenever a path parameter requires serialization (i.e. is decorated with[Path(PathSerializationMethod.Serialized)]).

This method wants you to return astring, which is the value that will be inserted in place of the placeholder in the path string.

There is no default path serializer, as its usage is often very specific. In order to usePathSerializationMethod.Serialized, youmust setRestClient.RequestPathParamSerializer.

There is aStringEnumRequestPathParamSerializer provided with RestEase designed for serializing enums that haveEnumMember,DisplayName orDisplay attributes specified on their members (evaluated in that order).This can be used as-is or as a reference for your own implementation.

To tell RestEase to use a path serializer, you must create a newRestClient, assign itsRequestPathParamSerializer property, then callFor<T>() to get an implementation of your interface.

For example:

varapi=newRestClient("https://api.example.com"){RequestPathParamSerializer=newStringEnumRequestPathParamSerializer(),}.For<ISomeApi>();

If you specified aFormat property on the[Path] attribute, this will be available asinfo.Format. By default, this isnull.

Controlling query string generation:QueryStringBuilder

RestEase has logic to turn a collection of query parameters into a single suitably-encoded query string. However, some servers don't correctly decode query strings, and so users may want to control how query strings are encoded.

To do this, subclassQueryStringBuilder and assign it to theRestClient.QueryStringBuilder property. See the methodBuildQueryParam inRequester for the default implementation.

Controlling the Requests

RestEase provides two ways for you to manipulate how exactly requests are made, before you need to resort toCustomizing RestEase.

RequestModifier

The first is aRestClient.For<T> overload which lets you specify a delegate which is invoked whenever a request is made. This allows you to inspect and alter the request in any way you want: changing the content, changing the headers, make your own requests in the meantime, etc.

For example, if you need to refresh an oAuth access token occasionally (using theADAL library as an example):

publicinterfaceIMyRestService{[Get("getPublicInfo")]Task<Foobar>SomePublicMethodAsync();[Get("secretStuff")][Header("Authorization","Bearer")]Task<Location>GetLocationOfRebelBaseAsync();}AuthenticationContextcontext=newAuthenticationContext(...);IGitHubApiapi=RestClient.For<IGitHubApi>("http://api.github.com",async(request,cancellationToken)=>{// See if the request has an authorize headervarauth=request.Headers.Authorization;if(auth!=null){// The AquireTokenAsync call will prompt with a UI if necessary// Or otherwise silently use a refresh token to return a valid access tokenvartoken=awaitcontext.AcquireTokenAsync("http://my.service.uri/app","clientId",newUri("callback://complete")).ConfigureAwait(false);request.Headers.Authorization=newAuthenticationHeaderValue(auth.Scheme,token);}});

If you need, you can get theIRequestInfo for the current request usingrequest.Options.TryGetValue(RestClient.HttpRequestMessageRequestInfoOptionsKey, out var requestInfo) (for .NET 5+), or(IRequestInfo)request.Properties[RestClient.HttpRequestMessageRequestInfoPropertyKey] (pre .NET 5).

CustomHttpClient

The second is aRestClient.For<T> overload which lets you specify a customHttpClient to use.This lets you customize theHttpClient, e.g. to set the request timeout. It also lets you specify a customHttpMessageHandler subclass, which allows you to control all sorts of things.

For example, if you wanted to 1) adjust the request timeout, and 2) allow invalid certificates (although the same approach would apply if you wanted to customize how certificates are validated), you could do something like this. Note thatWebRequestHandler is aHttpMessageHandler subclass which allows you to specify things likeServerCertificateValidationCallback.

publicclassCustomHttpClientHandler:WebRequestHandler{// Let's log all of our requests!privatestaticreadonlyLoggerlogger=LogManager.GetCurrentClassLogger();publicCustomHttpClientHandler(){// Allow any cert, valid or invalidthis.ServerCertificateValidationCallback=(sender,certificate,chain,sslPolicyErrors)=>true;}protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){if(logger.IsTraceEnabled){varresponse=awaitbase.SendAsync(request,cancellationToken);logger.Trace((awaitresponse.Content.ReadAsStringAsync()).Trim());returnresponse;}else{returnawaitbase.SendAsync(request,cancellationToken);}}}varhttpClient=newHttpClient(newCustomHttpClientHandler()){BaseAddress=newUri("https://secure-api.example.com"),Timeout=TimeSpan.FromSeconds(3),// Very slow to respond, this server};ISomeApiapi=RestClient.For<ISomeApi>(httpClient);

If you need, you can get theIRequestInfo for the current request using(IRequestInfo)request.Properties[RestClient.HttpRequestMessageRequestInfoPropertyKey].

Adding toHttpRequestMessage.Properties

In very specific cases (i.e. you use a customHttpMessageHandler), it might be useful to pass an object reference into the handler. In such caseHttpRequestMessage.Properties can be used.This is done by decorating method parameters with[HttpRequestMessageProperty]. If key parameter is not specified then the name of the parameter will be used.

If all (or most) of the methods on the interface pass such object you can specify a[HttpRequestMessageProperty] property. These work in the same way as path parameters, but they're on the level of the entire API. Properties must have both a getter and a setter.

Property keys used at interface method level must be unique: a parameter key must not be same as a property key.

For example:

publicinterfaceISomeApi{[HttpRequestMessageProperty]CommonDataAlwaysIncluded{get;set;}[Get]TaskGet([HttpRequestMessageProperty("myKey")]stringmyValueIWantToAccessFromHttpMessageHandler);}

In yourHttpMessageHandler subclass:

protectedoverrideasyncTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){varalwaysIncluded=(CommonData)request.Properties["AlwaysIncluded"];varmyValueIWantToAccessFromHttpMessageHandler=(string)request.Properties["myKey"];// Let's use the properties!returnawaitbase.SendAsync(request,cancellationToken).ConfigureAwait(false);}

Customizing RestEase

You've already seen how tospecify custom Serializers and Deserializers, andcontrol requests.

RestEase has been written in a way which makes it very easy to customize exactly how it works. In order to describe this, I'm first going to have to outline its architecture.

Given an API like:

publicinterfaceISomeApi{[Get("users/{userId}")]TaskGetUserAsync([Path]stringuserId);}

CallingRestClient.For<ISomeApi>(...) will cause a class like this to be generated:

namespaceRestEase.AutoGenerated{publicclassISomeApi{privatereadonlyIRequesterrequester;publicISomeApi(IRequesterrequester){this.requester=requester;}publicTaskGetUserAsync(stringuserId){varrequestInfo=newRequestInfo(HttpMethod.Get,"users/{userId}");requestInfo.AddPathParameter<string>("userId",userId);returnthis.requester.RequestVoidAsync(requestInfo);}}}

Now, you cannot customize what this generated class looks like, but you can see it doesn't actually do very much: it just builds up aRequestInfo object, then sends it off to theIRequester (which does all of the hard work). What youcan do however is to provide your ownIRequester implementation, and pass that to an appropriate overload ofRestClient.For<T>. In fact, the default implementation ofIRequester,Requester, has been carefully written so that it's easy to extend: each little bit of functionality is broken out into its own virtual method, so it's easy to replace just the behaviour you need.

Have a read throughRequester, figure out what you want to change, subclass it, and provide an instance of that subclass toRestClient.For<T>.

Interface Accessibility

Since RestEase generates an interface implementation in a separate assembly, the interface ideally needs to be public.

If you don't want to do this, you'll need to mark RestEase as being a 'friend' assembly, which allows RestEase to see your internal types. Add the following line to yourAssemblyInfo.cs:

[assembly: InternalsVisibleTo(RestEase.RestClient.FactoryAssemblyName)]

You can place the interface inside any namespace, or nest the interface inside another public type if you wish.

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. RestEase supports these, allowing you to define a single API interface with a generic type:

publicinterfaceIReallyExcitingCrudApi<T,TKey>{[Post("")]Task<T>Create([Body]Tpaylod);[Get("")]Task<List<T>>ReadAll();[Get("{key}")]Task<T>ReadOne([Path]TKeykey);[Put("{key}")]TaskUpdate([Path]TKeykey,[Body]Tpayload);[Delete("{key}")]TaskDelete([Path]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=RestClient.For<IReallyExcitingCrudApi<User,string>>("https://api.example.com/users");

Note that RestEase makes certain choices about how parameters and the return type are processed when the implementation of the interface is generated, and not when it is known (and the exact parameter types are known). This means that, for example, if you declare a return type ofTask<T>, then call withT set toString, then you will not get a stream back - the response will be deserialized as a stream, which will almost certainly fail. Likewise if you declare a query parameter of typeT, then setT toIEnumerable<string>, then your query will contain something likeString[], instead of a collection of query parameters.

Using Generic Methods

You can define generic methods, if you wish. These have all of the same caveats as generic interfaces.

publicinterfaceIReallyExcitingCrudApi{[Post("")]Task<T>Create<T>([Body]Tpaylod);[Get("")]Task<List<T>>ReadAll<T>();[Get("{key}")]Task<T>ReadOne<T,TKey>(TKeykey);[Put("{key}")]TaskUpdate<T,TKey>(TKeykey,[Body]Tpayload);[Delete("{key}")]TaskDelete<TKey>(TKeykey);}

Interface Inheritance

Sharing common properties and methods

You're allowed to use interface inheritance to share common properties and methods between different APIs.

You can only put an[AllowAnyStatusCode] attribute on the derived interface, and not on any parent interfaces. An[AllowAnyStatusCode] attribute on the derived interface also applies to all methods on all parent interfaces.

For example:

publicinterfaceIAuthenticatedEndpoint{[Header("X-Api-Token")]stringApiToken{get;set;}[Header("X-Api-Username")]stringApiUsername{get;set;}[Path("userId")]intUserId{get;set;}}publicinterfaceIDevicesEndpoint:IAuthenticatedEndpoint{[Get("/devices")]Task<IList<Device>>GetAllDevices([QueryMap]IDictionary<string,string>filters);}publicinterfaceIUsersEndpoint:IAuthenticatedEndpoint{[Get("/user/{userId}")]Task<User>FetchUserAsync();}

IDisposable

If your interface implementsIDisposable, then RestEase will generate aDispose() method which disposes the underlyingHttpClient. Do this if you want to be able to dispose theHttpClient.

Advanced Functionality Using Extension Methods

Sometimes you'll have cases where you want to do something that's more complex than can be achieved using RestEase alone (e.g. uploading multipart form data), but you still want to provide a nice interface to consumers. One option is to write extension methods on your interface. There are two ways of doing this.

Wrapping other methods

The easiest thing to do is to put a method on your interface which won't be called by your code, but which can be wrapped by your extension method. This approach is unit testable.

publicinterfaceISomeApi{// Method which is only used by SomeApiExtensions[Post("upload")]TaskUploadAsync([Body]HttpContentcontent);}publicstaticclassSomeApiExtensions{publicstaticTaskUploadAsync(thisISomeApiapi,byte[]imageData,stringtoken){varcontent=newMultipartFormDataContent();varimageContent=newByteArrayContent(imageData);imageContent.Headers.ContentType=newMediaTypeHeaderValue("image/jpeg");content.Add(imageContent);vartokenContent=newFormUrlEncodedContent(new[]{newKeyValuePair<string,string>("token",token)});content.Add(tokenContent);returnapi.UploadAsync(content);}}

UsingIRequester directly

Alternatively, you can put a property of typeIRequester on your interface, then write an extension method which uses theIRequester. Note that the attributes or properties you put on your interface (ISomeApi in the example below) will not be added to theRequestInfo, since you are not invoking any code which does this. Note also that this approach is not unit testable.

publicinterfaceISomeApi{IRequesterRequester{get;}}publicstaticclassSomeApiExtensions{publicstaticTaskUploadAsync(thisISomeApiapi,byte[]imageData,stringtoken){varcontent=newMultipartFormDataContent();varimageContent=newByteArrayContent(imageData);imageContent.Headers.ContentType=newMediaTypeHeaderValue("image/jpeg");content.Add(imageContent);vartokenContent=newFormUrlEncodedContent(new[]{newKeyValuePair<string,string>("token",token)});content.Add(tokenContent);varrequestInfo=newRequestInfo(HttpMethod.Post,"upload");requestInfo.SetBodyParameterInfo(BodySerializationMethod.Default,content);returnapi.Requester.RequestVoidAsync(requestInfo);}}

IRequester andRequestInfo are not documented in this README. Read the doc comments.

FAQs

I want to use Basic Authentication

Something like this...

publicinterfaceISomeApi{[Header("Authorization")]AuthenticationHeaderValueAuthorization{get;set;}[Get("foo")]TaskDoSomethingAsync();}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");varvalue=Convert.ToBase64String(Encoding.ASCII.GetBytes("username:password1234"));api.Authorization=newAuthenticationHeaderValue("Basic",value);awaitapi.DoSomethingAsync();

I need to request an absolute path

Sometimes your API responses will contain absolute URLs, for example a "next page" link. Therefore you'll want a way to request a resource using an absolute URL which overrides the base URL you specified.

Thankfully this is easy: if you give an absolute URL to e.g.[Get("https://api.example.com/foo")], then the base URL will be ignored. You will also need to disable URL encoding.

publicinterfaceISomeApi{[Get("users")]Task<UsersResponse>FetchUsersAsync();[Get("{url}")]Task<UsersResponse>FetchUsersByUrlAsync([Path(UrlEncode=false)]stringurl);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");varfirstPage=awaitapi.FetchUsersAsync();// Actually put decent logic here...varsecondPage=awaitapi.FetchUsersByUrlAsync(firstPage.NextPage);

I may get responses in both XML and JSON, and want to deserialize both

Occasionally you get an API which can return both JSON and XML (apparently...). In this case, you'll want to auto-detect what sort of response you got, and deserialize with an appropriate deserializer.

To do this, use a custom deserializer, which can do this detection.

publicclassHybridResponseDeserializer:ResponseDeserializer{privateTDeserializeXml<T>(stringcontent){// Consider caching generated XmlSerializersvarserializer=newXmlSerializer(typeof(T));using(varstringReader=newStringReader(content)){return(T)serializer.Deserialize(stringReader);}}privateTDeserializeJson<T>(stringcontent){returnJsonConvert.Deserialize<T>(content);}publicoverrideTDeserialize<T>(stringcontent,HttpResponseMessageresponse){switch(response.Content.Headers.ContentType.MediaType){case"application/json":returnthis.DeserializeJson<T>(content);case"application/xml":returnthis.DeserializeXml<T>(content);}thrownewArgumentException("Response was not JSON or XML");}}varapi=RestClient.For<ISomeApi>("https://api.example.com",newHybridResponseDeserializer());

Is RestEase thread safe?

Yes. It is safe to create implementations of interfaces from multiple threads at the same time (and to create multiple implementations of the same interface), and it is safe to use an implementation from multiple threads at the same time.

I want to upload a file

Let's assume you want to upload a file (from a stream), setting its name and content-type manually (skip these bits of not). There are a couple of ways of doing this, depending on your needs:

publicinterfaceISomeApi{[Header("Content-Disposition","form-data; filename=\"somefile.txt\"")][Header("Content-Type","text/plain")][Post("upload")]TaskUploadFileVersionOneAsync([Body]Streamfile);[Post("upload")]// You can use strings instead of strongly-typed header values, if you wantTaskUploadFileVersionTwoAsync([Header("Content-Disposition")]ContentDispositionHeaderValuecontentDisposition,[Header("Content-Type")]MediaTypeHeaderValuecontentType,[Body]Streamfile);[Post("upload")]TaskUploadFileVersionThreeAsync([Body]HttpContentcontent);}ISomeApiapi=RestClient.For<ISomeApi>("https://api.example.com");// Version one (constant headers)using(varfileStream=File.OpenRead("somefile.txt")){awaitapi.UploadFileVersionOneAsync(fileStream);}// Version two (variable headers)using(varfileStream=File.OpenRead("somefile.txt")){varcontentDisposition=newContentDispositionHeaderValue("form-header"){FileName="\"somefile.txt\""};varcontentType=newMediaTypeHeaderValue("text/plain");awaitapi.UploadFileVersionTwoAsync(contentDisposition,contentType,fileStream);}// Version three (precise control over HttpContent)using(varfileStream=File.OpenRead("somefile.txt")){varfileContent=newStreamContent(fileStream);fileContent.Headers.ContentDisposition=newContentDispositionHeaderValue("form-header"){FileName="\"somefile.txt\""};fileContent.Headers.ContentType=newMediaTypeHeaderValue("text/plain");awaitapi.UploadFileVersionThreeAsync(fileContent);}

Obviously, set the headers you need - don't just copy me blindly.

You can useextension methods to make this more palatable for consumers.

I want to ensure that all of the required properties on my request body are set

User @netclectic has a solution,see this issue.

About

Easy-to-use typesafe REST API client library for .NET Standard 1.1 and .NET Framework 4.5 and higher, which is simple and customisable. Inspired by Refit

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp