本文大部份內容是針對Refit官網的翻譯。react
官網地址: https://github.com/reactiveui/refitgit
Refit是一個相似於Retrofit的Restful Api庫,使用它,你能夠將你的Restful Api定義在接口中。github
例如:json
public interface IGitHubApi { [Get("/users/{user}")] Task<User> GetUser(string user); }
這裏RestService
類生成了一個IGitHubApi
接口的實現,它使用HttpClient
來進行api調用。c#
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com"); var octocat = await gitHubApi.GetUser("octocat");
當前Refit支持一下平臺。api
.NET Core的注意事項:數組
對於.NET Core的構建時支持(Build-Time support), 你必須使用.NET Core 2.x SDK。你能夠針對全部的支持平臺構建你的庫,只要構建時使用2.x SDK便可。app
針對每一個方法都必須提供一個HTTP屬性,這個屬性指定了請求的方式和相關的URL。這裏有6種內置的批註:Get, Post, Put, Delete, Patch和Head。在批註中須要指定資源對應的URL。async
[Get("/users/list")]
你一樣能夠指定URL中的查詢字符串。ide
[Get("/users/list?sort=desc")]
你還可使用可替換塊(replacement block)和方法參數建立動態URL。這裏可替換塊是一個被大括號包裹的字符串變量。
[Get("/group/{id}/users")] Task<List<User>> GroupList([AliasAs("id")] int groupId);
URL中沒有指定的參數,就會自動做爲URL的查詢字符串。這與Retrofit不一樣,在Retrofit中全部參數都必須顯示指定。
[Get("/group/{id}/users")] Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);
這裏當調用GroupList(4, "desc");
方法時,調用API會是"/group/4/users?sort=desc"
。
迴轉路由參數語法:使用雙星號的捕獲全部參數(catch-all parameter)且不會對"/"進行編碼,
在生成連接的過程, 路由系統將編碼雙星號捕獲的所有參數(catch-all parameter),而不會編碼"/"。
[Get("/search/{**page}")] Task<List<Page>> Search(string page);
迴轉路由參數必須是字符串
這裏當調用Search("admin/products");
時,生成的鏈接是"/search/admin/products"
當你指定一個對象做爲查詢參數的時候,全部非空的public屬性將被用做查詢參數。使用Query
特性將改變默認的行爲,它會扁平化你的查詢字符串對象。若是使用Query
特性,你還能夠針對扁平化查詢字符串對象添加指定的分隔符和前綴。
例:
public class MyQueryParams { [AliasAs("order")] public string SortOrder { get; set; } public int Limit { get; set; } }
普通的扁平化查詢字符串對象:
[Get("/group/{id}/users")] Task<List<User>> GroupList([AliasAs("id")] int groupId, MyQueryParams params);
扁平化查詢字符串對象並附加分隔符和前綴
[Get("/group/{id}/users")] Task<List<User>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".","search")] MyQueryParams params);
代碼調用及結果。
params.SortOrder = "desc"; params.Limit = 10; GroupList(4, params) //結果 "/group/4/users?order=desc&Limit=10" GroupListWithAttribute(4, params) //結果 "/group/4/users?search.order=desc&search.Limit=10"
Query
特性一樣能夠指定查詢字符串中應該如何格式化集合對象。
例:
[Get("/users/list")] Task Search([Query(CollectionFormat.Multi)]int[] ages); Search(new [] {10, 20, 30}) //結果 "/users/list?ages=10&ages=20&ages=30" [Get("/users/list")] Task Search([Query(CollectionFormat.Csv)]int[] ages); Search(new [] {10, 20, 30}) //結果 "/users/list?ages=10%2C20%2C30"
在你的方法簽名中,你還能夠將使用Body
特性將參數中的一個標記爲正文內容。
[Post("/users/new")] Task CreateUser([Body] User user);
這裏Refit支持4種請求體數據
Stream
, 其內容會包裹在一個StreamContent
對象中。string
, 其內容會直接用做正文內容。當指定當前參數擁有特性[Body(BodySerializationMethod.Json)]
時,它會被包裹在一個StringContent
對象中。[Body(BodySerializationMethod.UrlEncoded)]
, 其內容會被URL編碼。默認狀況下,Refit會流式傳輸正文內容,而不會緩衝它。這意味着,你能夠從磁盤流式傳輸文件,而不產生將整個文件加載到內存中的開銷。這樣作的缺點是,請求頭部沒有設置Content-Length
。若是你的API須要發送一個請求並指定Content-Length
請求頭,則須要將Body
特性的buffered
參數設置爲true。
Task CreateUser([Body(buffered: true)] User user);
JSON請求和響應可使用Json.NET來序列化和反序列化,默認狀況下,Refit會使用Newtonsoft.Json.JsonConvert.DefaultSettings
的默認序列化配置。
JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = {new StringEnumConverter()} }; // Serialized as: {"day":"Saturday"} await PostSomeStuff(new { Day = DayOfWeek.Saturday });
由於默認設置是全局設置,它會影響你的整個應用。因此這裏咱們最好使用針對特定API使用獨立的配置。當使用Refit生成一個接口對象的時候,你能夠傳入一個RefitSettings
參數,這個參數能夠指定你使用的JSON序列化配置。
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com", new RefitSettings { ContentSerializer = new JsonContentSerializer( new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver() } )}); var otherApi = RestService.For<IOtherApi>("https://api.example.com", new RefitSettings { ContentSerializer = new JsonContentSerializer( new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() } )});
針對自定義屬性的序列化和反序列化,咱們一樣可使用Json.NET的JsonProperty
屬性。
public class Foo { // Works like [AliasAs("b")] would in form posts (see below) [JsonProperty(PropertyName="b")] public string Bar { get; set; } }
針對XML請求和響應的序列化和反序列化,Refit使用了System.Xml.Serialization.XmlSerializer
。默認狀況下, Refit會使用JSON內容序列化器,若是想要使用XML內容序列化器,你須要將RefitSetting
的ContentSerializer
屬性指定爲XmlContentSerializer
。
var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML", new RefitSettings { ContentSerializer = new XmlContentSerializer() });
咱們一樣可使用System.Xml.Serialization
命名空間下的特性,自定義屬性的序列化和反序列化。
public class Foo { [XmlElement(Namespace = "https://www.w3.org/XML")] public string Bar { get; set; } }
System.Xml.Serialization.XmlSerializer
提供了多種序列化方式,你能夠經過在XmlContentSerialier
對象的構造函數中指定一個XmlContentSerializerSettings
對象類進行配置。
var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML", new RefitSettings { ContentSerializer = new XmlContentSerializer( new XmlContentSerializerSettings { XmlReaderWriterSettings = new XmlReaderWriterSettings() { ReaderSettings = new XmlReaderSettings { IgnoreWhitespace = true } } } ) });
針對採用表單Post的API( 正文會被序列化成application/x-www-form-urlencoded ), 咱們能夠將指定參數的正文特性指定爲BodySerializationMethod.UrlEncoded
。
這個參數能夠是字典IDictionary
接口對象。
public interface IMeasurementProtocolApi { [Post("/collect")] Task Collect([Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> data); } var data = new Dictionary<string, object> { {"v", 1}, {"tid", "UA-1234-5"}, {"cid", new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")}, {"t", "event"}, }; // 序列化爲: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.Collect(data);
固然參數也能夠是一個普通對象,Refit會將對象中全部public, 可讀取的屬性序列化成表單字段。固然這裏你可使用AliasAs
特性,爲序列化的表單字段起別名。
public interface IMeasurementProtocolApi { [Post("/collect")] Task Collect([Body(BodySerializationMethod.UrlEncoded)] Measurement measurement); } public class Measurement { // Properties can be read-only and [AliasAs] isn't required public int v { get { return 1; } } [AliasAs("tid")] public string WebPropertyId { get; set; } [AliasAs("cid")] public Guid ClientId { get; set; } [AliasAs("t")] public string Type { get; set; } public object IgnoreMe { private get; set; } } var measurement = new Measurement { WebPropertyId = "UA-1234-5", ClientId = new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"), Type = "event" }; // 序列化爲: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.Collect(measurement);
若是當前屬性同時指定了[JsonProperty(PropertyName)]
和AliasAs()
, Refit會優先使用AliasAs()
中指定的名稱。這意味着,如下類型會被序列化成one=value1&two=value2
public class SomeObject { [JsonProperty(PropertyName = "one")] public string FirstProperty { get; set; } [JsonProperty(PropertyName = "notTwo")] [AliasAs("two")] public string SecondProperty { get; set; } }
注意:
AliasAs
只能應用在請求參數和Form正文Post中,不能應用於響應對象。若是要爲響應對象屬性起別名,你依然須要使用[JsonProperty("full-property-name")]
你可使用Headers
特性指定一個或多個靜態的請求頭。
[Headers("User-Agent: Awesome Octocat App")] [Get("/users/{user}")] Task<User> GetUser(string user);
爲了簡便使用,你也能夠將Headers
特性放在接口定義上,從而使當前接口中定義的全部Rest請求都添加相同的靜態頭。
[Headers("User-Agent: Awesome Octocat App")] public interface IGitHubApi { [Get("/users/{user}")] Task<User> GetUser(string user); [Post("/users/new")] Task CreateUser([Body] User user); }
若是頭部內容須要在運行時動態設置,你能夠在方法簽名處,使用Header
特性指定一個動態頭部參數,你能夠在調用Api時,爲這個參數指定一個dynamic
類型的值,從而實現動態頭。
[Get("/users/{user}")] Task<User> GetUser(string user, [Header("Authorization")] string authorization); // Will add the header "Authorization: token OAUTH-TOKEN" to the request var user = await GetUser("octocat", "token OAUTH-TOKEN");
使用請求頭的最多見場景就是受權。當今絕大多數的API都是使用OAuth, 它會提供一個帶過時時間的access token和一個負責刷新access token的refresh token。
爲了封裝這些受權令牌的使用,咱們能夠自定義一個HttpClientHandler
。
class AuthenticatedHttpClientHandler : HttpClientHandler { private readonly Func<Task<string>> getToken; public AuthenticatedHttpClientHandler(Func<Task<string>> getToken) { if (getToken == null) throw new ArgumentNullException(nameof(getToken)); this.getToken = getToken; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // See if the request has an authorize header var auth = request.Headers.Authorization; if (auth != null) { var token = await getToken().ConfigureAwait(false); request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token); } return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } }
雖然HttpClient
包含了幾乎相同的方法簽名,可是它的使用方式不一樣。Refit不會調用HttpClient.SendAsync
方法,這裏必須使用自定義的HttpClientHandler
替換它。
class LoginViewModel { AuthenticationContext context = new AuthenticationContext(...); private async Task<string> GetToken() { // The AcquireTokenAsync call will prompt with a UI if necessary // Or otherwise silently use a refresh token to return // a valid access token var token = await context.AcquireTokenAsync("http://my.service.uri/app", "clientId", new Uri("callback://complete")); return token; } public async Task LoginAndCallApi() { var api = RestService.For<IMyRestService>(new HttpClient(new AuthenticatedHttpClientHandler(GetToken)) { BaseAddress = new Uri("https://the.end.point/") }); var location = await api.GetLocationOfRebelBase(); } } interface IMyRestService { [Get("/getPublicInfo")] Task<Foobar> SomePublicMethod(); [Get("/secretStuff")] [Headers("Authorization: Bearer")] Task<Location> GetLocationOfRebelBase(); }
在以上代碼中,當任何須要身份驗證的的方法被調用的時候,AuthenticatedHttpClientHandler
會嘗試獲取一個新的access token。 這裏程序會檢查access token是否到期,並在須要時獲取新的令牌。
當一個接口方法被指定爲[Multipart]
, 這意味着當前Api提交的內容中包含分段內容類型。針對分段方法,Refit當前支持一下幾種參數類型
這裏參數名會做爲分段數據的字段名。固然你能夠用AliasAs
特性複寫它。
爲了給二進制數組,Stream流以及FileInfo參數的內容指定文件名和內容類型,咱們必需要使用封裝類。Refit中默認的封裝類有3種,ByteArrarPart
, StreamPart
, FileInfoPart
。
public interface ISomeApi { [Multipart] [Post("/users/{id}/photo")] Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream); }
爲了將一個Stream流對象傳遞給以上定義的方法,咱們須要構建一個StreamObject
對象:
someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));
爲了封裝可能來自服務的任何異常,你能夠捕獲包含請求和響應信息的ApiException
。 Refit還支持捕獲因爲不良請求而引起的驗證異常,以解決問題詳細信息。 有關驗證異常的問題詳細信息的特定信息,只需捕獲ValidationApiException
:
// ... try { var result = await awesomeApi.GetFooAsync("bar"); } catch (ValidationApiException validationException) { // handle validation here by using validationException.Content, // which is type of ProblemDetails according to RFC 7807 } catch (ApiException exception) { // other exception handling } // ...