本篇文章的內容是對上一篇.net的retrofit--WebApiClient庫的深層次補充,你可能須要先閱讀上一篇才能理解此篇文章。本文將詳細地講解WebApiClient的原理,結合實際項目中可能遇到的問題進行使用說明。html
WebApiClient是開源在github上的一個httpClient客戶端庫,內部基於HttpClient開發,是一個只須要定義c#接口(interface),並打上相關特性,便可異步調用http-api的框架 ,支持.net framework4.5+、netcoreapp2.0和netstandard2.0。git
var config = new HttpApiConfig { // 請求的域名,會覆蓋[HttpHost]特性 HttpHost = new Uri("http://www.webapiclient.com"), }; var myWebApi = HttpApiClient.Create<MyWebApi>(config);
當序列化一個多屬性的模型時,FormatOptions能夠約束DateTime類型的屬性值轉換爲字符串的格式,也能夠指定屬性名是爲CamelCase。github
首次獲取HttpClient實例時,HttpClient的實例將被建立,HttpClient屬性是一個IHttpClient接口,是對HttpClient對象的包裝,它的Handler暴露出與HttpClient關聯的HttpClientHandler對象。web
GlobalFilters用於添加全局過濾器,這些過濾器不須要使用硬編碼修飾於接口,而是經過配置傳輸給接口的實例,適用於接口定義的項目和接口調用的項目分離開的項目結構。json
1 建立接口實現類
當調用WebApiClient.Create時,內部使用Emit建立接口的實現類,該實現類爲接口的每一個方法實現爲:獲取方法信息和調用參數值傳給攔截器(IApiInterceptor)處理;c#
2 攔截器建立ITask任務
IApiInterceptor收到方法的調用時,根據方法信息和參數值建立Api描述對象ApiActionDescriptor,而後將和HttpApiConfig實例和ApiActionDescriptor包裝成ITask任務對象並返回;api
3 等待調用者執行請求
當調用者await ITask 或 await ITask.InvokeAsync()時,建立ApiActionContext並按照順序執行ApiActionContext裏描述的各類Attribute,這些Attribue影響着ApiActionContext的HttpRequestMessage等屬性對象,而後使用HttpClient發送這個HttpRequestMessage對象,獲得HttpResponseMessage,最後將HttpResponseMessage的Content轉換爲接口的返回值;app
/// <summary> /// 異步執行api /// </summary> /// <param name="context">上下文</param> /// <returns></returns> public async Task<object> ExecuteAsync(ApiActionContext context) { var apiAction = context.ApiActionDescriptor; var globalFilters = context.HttpApiConfig.GlobalFilters; foreach (var actionAttribute in apiAction.Attributes) { await actionAttribute.BeforeRequestAsync(context); } foreach (var parameter in apiAction.Parameters) { foreach (var parameterAttribute in parameter.Attributes) { await parameterAttribute.BeforeRequestAsync(context, parameter); } } foreach (var filter in globalFilters) { await filter.OnBeginRequestAsync(context); } foreach (var filter in apiAction.Filters) { await filter.OnBeginRequestAsync(context); } await this.SendAsync(context); foreach (var filter in globalFilters) { await filter.OnEndRequestAsync(context); } foreach (var filter in apiAction.Filters) { await filter.OnEndRequestAsync(context); } return await apiAction.Return.Attribute.GetTaskResult(context); }
WebApiClient內置不少特性,包含接口級、方法級、參數級的,他們分別是實現了IApiActionAttribute接口、IApiActionFilterAttribute接口、IApiParameterAttribute接口、IApiParameterable接口和IApiReturnAttribute接口的一個或多個接口。通常狀況下內置的特性就足以夠用,但實際項目中,你可能會遇到個別特殊的場景,須要本身實現一些特性或過濾器,主要用來操控請求上下文的RequestMessage對象,影響請求對象。框架
舉個例子:好比,服務端要求使用x-www-form-urlencoded提交,因爲接口設計不合理,目前要求是提交:fieldX= {X}的json文本&fieldY={Y}的json文本 這裏{X}和{Y}都是一個多字段的Model,咱們對應的接口是這樣設計的:異步
[HttpHost("/upload")] ITask<bool> UploadAsync( [FormField][AliasAs("fieldX")] string xJson, [FormField][AliasAs("fieldY")] string yJson);
顯然,咱們接口參數爲string類型的範圍太廣,沒有約束性,咱們但願是這樣子:
[HttpHost("/upload")] ITask<bool> UploadAsync([FormFieldJson] X fieldX, [FormFieldJson] Y fieldY);
如今咱們爲這種特殊場景實現一個[FormFieldJson]的參數級特性,給每一個參數修飾這個[FormFieldJson]後,參數就解釋爲其序列化爲Json的文本,作爲表單的一個字段內容:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] class FormFieldJson: Attribute, IApiParameterAttribute { public async Task BeforeRequestAsync(ApiActionContext context, ApiParameterDescriptor parameter) { var options = context.HttpApiConfig.FormatOptions; var json = context.HttpApiConfig.JsonFormatter.Serialize(parameter.Value, options); var fieldName = parameter.Name; await context.RequestMessage.AddFormFieldAsync(fieldName, json); } }
舉個例子:咱們須要爲每一個請求的url額外的動態添加一個叫sign的參數,這個sign可能和配置文件等有關係,並且每次都須要計算:
class SignFilter : ApiActionFilterAttribute { public override Task OnBeginRequestAsync(ApiActionContext context) { var sign = DateTime.Now.Ticks.ToString(); context.RequestMessage.AddUrlQuery("sign", sign); return base.OnBeginRequestAsync(context); } } [SignFilter] public interface MyApi : IDisposable { ... }
class GlobalFilter : IApiActionFilter { public Task OnBeginRequestAsync(ApiActionContext context) { if (context.ApiActionDescriptor.Member.IsDefined(typeof(MyCustomAttribute), true)) { // do something } return Task.CompletedTask; } public Task OnEndRequestAsync(ApiActionContext context) { return Task.CompletedTask; } } // 經過配置項將全局過濾器傳給MyWebApi實例 var config = new HttpApiConfig(); config.GlobalFilters.Add(new GlobalFilter()); var myWebApi = HttpApiClient.Create<MyWebApi>(config);
在一些場景中,你的模型與服務須要的數據模塊可能不是所有吻合,DataAnnotations的功能能夠很是方便實現二者的對接,目前DataAnnotations只支持Json序列化和KeyValue序列化,xml序列化不受任何變化。
public class UserInfo { public string Account { get; set; } // 別名 [AliasAs("a_password")] public string Password { get; set; } // 時間格式,優先級最高 [DateTimeFormat("yyyy-MM-dd")] public DateTime? BirthDay { get; set; } // 忽略序列化 [IgnoreSerialized] public string Email { get; set; } // 時間格式 [DateTimeFormat("yyyy-MM-dd HH:mm:ss")] public DateTime CreateTime { get; set; } }
await ITask,實際是調用了ITask.GetAwaiter()方法,等於同於ITask.InvokeAsync().GetAwaiter()方法。因此await ITask等同於await ITask.InvokeAsync()
InvokeAsync()返回Task對象,實際是http請求的任務對象。一個ITask實例,能夠屢次調用InvokeAsync()方法,完成屢次如出一轍的請求。ITask的不少擴展,是對InvokeAsync方法調用的包裝而獲得。
Retry本質上是對ITask的InvokeAsync的包裝,實際思想是當符合某種條件時,就多調用一次InvokeAsync方法,達到重試提交請求的目的。
Handle也是對ITask的InvokeAsync的包裝,使用try catch對InvokeAsync方法封裝爲新的委託,當捕獲到符合條件的異常類型時,就返回某種結果。
var result = await myWebApi.TestAsync() .Retry(3, i => TimeSpan.FromSeconds(i)) .WhenCatch<Exception>() .HandleAsDefaultWhenException();
以上能夠解讀爲,當遇到異常時,再重試請求,累計重試3次仍是異常的話,處理爲返回null值,期間總共最多請求了4次。
HttpClient目前沒提供任何的同步請求方法,因此WebApiClient的請求也是同樣,若是遇到必須使用同步的場景,能夠暫時使用 ITask.GetAwaiter().GetResult()方法等待結果。