
本文大部分內容是針對Refit官網的翻譯。
官網地址: https://github.com/reactiveui/refit
Refit是一個類似於Retrofit的Restful Api庫,使用它,你可以將你的Restful Api定義在介面中。
例如:
public interface IGitHubApi{[Get("/users/{user}")]Task GetUser(string user);}
這裡RestService類生成了一個IGitHubApi介面的實現,它使用HttpClient來進行api呼叫。
var gitHubApi = RestService.For("https://api.github.com");var octocat = await gitHubApi.GetUser("octocat");
Refit可以在哪些地方使用?
當前Refit支援一下平臺。
•UWP•Xamarin.Android•Xamarin.Mac•Xamarin.iOS•Desktop .NET 4.6.1•.NET Core
.NET Core的註意事項:
對於.NET Core的構建時支援(Build-Time support), 你必須使用.NET Core 2.x SDK。你可以針對所有的支援平臺構建你的庫,只要構建時使用2.x SDK即可。
API屬性
基本用法
針對每個方法都必須提供一個HTTP屬性,這個屬性指定了請求的方式和相關的URL。這裡有6種內建的批註:Get, Post, Put, Delete, Patch和Head。在批註中需要指定資源對應的URL。
[Get("/users/list")]
你同樣可以指定URL中的查詢字串。
[Get("/users/list?sort=desc")]
動態URL
你還可以使用可替換塊(replacement block)和方法引數建立動態URL。這裡可替換塊是一個被大括號包裹的字串變數。
[Get("/group/{id}/users")]Task> GroupList([AliasAs("id")] int groupId);
URL中沒有指定的引數,就會自動作為URL的查詢字串。這與Retrofit不同,在Retrofit中所有引數都必須顯示指定。
[Get("/group/{id}/users")]Task> 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> 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> GroupList([AliasAs("id")] int groupId, MyQueryParams params);
扁平化查詢字串物件並附加分隔符和字首
[Get("/group/{id}/users")]Task> 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編碼。•針對其他型別,當前指定的引數會被預設序列化成JSON。
緩衝及Content-Header頭部設定
預設情況下,Refit會流式傳輸正文內容,而不會緩衝它。這意味著,你可以從磁碟流式傳輸檔案,而不產生將整個檔案載入到記憶體中的開銷。這樣做的缺點是,請求頭部沒有設定Content-Length。如果你的API需要傳送一個請求並指定Content-Length請求頭,則需要將Body特性的buffered引數設定為true。
Task CreateUser([Body(buffered: true)] User user);
Json內容
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("https://api.github.com",new RefitSettings {ContentSerializer = new JsonContentSerializer(new JsonSerializerSettings {ContractResolver = new SnakeCasePropertyNamesContractResolver()})});var otherApi = RestService.For("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內容
針對XML請求和響應的序列化和反序列化,Refit使用了System.Xml.Serialization.XmlSerializer。預設情況下, Refit會使用JSON內容序列化器,如果想要使用XML內容序列化器,你需要將RefitSetting的ContentSerializer屬性指定為XmlContentSerializer。
var gitHubApi = RestService.For("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("https://www.w3.org/XML",new RefitSettings {ContentSerializer = new XmlContentSerializer(new XmlContentSerializerSettings{XmlReaderWriterSettings = new XmlReaderWriterSettings(){ReaderSettings = new XmlReaderSettings{IgnoreWhitespace = true}}})});
表單Post
針對採用表單Post的API( 正文會被序列化成application/x-www-form-urlencoded ), 我們可以將指定引數的正文特性指定為BodySerializationMethod.UrlEncoded。
這個引數可以是字典IDictionary介面物件。
public interface IMeasurementProtocolApi{[Post("/collect")]Task Collect([Body(BodySerializationMethod.UrlEncoded)] Dictionary data);}var data = new Dictionary {{"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;=eventawait 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 requiredpublic 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;=eventawait 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")]
設定請求Header
靜態頭
你可以使用Headers特性指定一個或多個靜態的請求頭。
[Headers("User-Agent: Awesome Octocat App")][Get("/users/{user}")]Task GetUser(string user);
為了簡便使用,你也可以將Headers特性放在介面定義上,從而使當前介面中定義的所有Rest請求都新增相同的靜態頭。
[Headers("User-Agent: Awesome Octocat App")]public interface IGitHubApi{[Get("/users/{user}")]Task GetUser(string user);[Post("/users/new")]Task CreateUser([Body] User user);}
動態頭
如果頭部內容需要在執行時動態設定,你可以在方法簽名處,使用Header特性指定一個動態頭部引數,你可以在呼叫Api時,為這個引數指定一個dynamic型別的值,從而實現動態頭。
[Get("/users/{user}")]Task GetUser(string user, [Header("Authorization")] string authorization);// Will add the essay-header "Authorization: token OAUTH-TOKEN" to the requestvar user = await GetUser("octocat", "token OAUTH-TOKEN");
授權(動態頭的升級版)
使用請求頭的最常見場景就是授權。當今絕大多數的API都是使用OAuth, 它會提供一個帶過期時間的access token和一個負責掃清access token的refresh token。
為了封裝這些授權令牌的使用,我們可以自定義一個HttpClientHandler。
class AuthenticatedHttpClientHandler : HttpClientHandler{private readonly Func> getToken;public AuthenticatedHttpClientHandler(Func> getToken){if (getToken == null) throw new ArgumentNullException(nameof(getToken));this.getToken = getToken;}protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){// See if the request has an authorize essay-headervar 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 GetToken(){// The AcquireTokenAsync call will prompt with a UI if necessary// Or otherwise silently use a refresh token to return// a valid access tokenvar token = await context.AcquireTokenAsync("http://my.service.uri/app", "clientId", new Uri("callback://complete"));return token;}public async Task LoginAndCallApi(){var api = RestService.For(new HttpClient(new AuthenticatedHttpClientHandler(GetToken)) { BaseAddress = new Uri("https://the.end.point/") });var location = await api.GetLocationOfRebelBase();}}interface IMyRestService{[Get("/getPublicInfo")]Task SomePublicMethod();[Get("/secretStuff")][Headers("Authorization: Bearer")]Task GetLocationOfRebelBase();}
在以上程式碼中,當任何需要身份驗證的的方法被呼叫的時候,AuthenticatedHttpClientHandler會嘗試獲取一個新的access token。 這裡程式會檢查access token是否到期,併在需要時獲取新的令牌。
分段上傳
當一個介面方法被指定為[Multipart], 這意味著當前Api提交的內容中包含分段內容型別。針對分段方法,Refit當前支援一下幾種引數型別
•字串•二進位制陣列•Stream流•FileInfo
這裡引數名會作為分段資料的欄位名。當然你可以用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}// ...
知識星球
朋友會在“發現-看一看”看到你“在看”的內容