.net的retrofit--WebApiClient庫深刻篇

前言

本篇文章的內容是對上一篇.net的retrofit--WebApiClient庫的深層次補充,你可能須要先閱讀上一篇才能理解此篇文章。本文將詳細地講解WebApiClient的原理,結合實際項目中可能遇到的問題進行使用說明。html

庫簡介

WebApiClient是開源在github上的一個httpClient客戶端庫,內部基於HttpClient開發,是一個只須要定義c#接口(interface),並打上相關特性,便可異步調用http-api的框架 ,支持.net framework4.5+、netcoreapp2.0和netstandard2.0。git

1. HttpApiConfig的使用

1.1 建立HttpApiConfig

var config = new HttpApiConfig
{
    // 請求的域名,會覆蓋[HttpHost]特性
    HttpHost = new Uri("http://www.webapiclient.com"),
};
var myWebApi = HttpApiClient.Create<MyWebApi>(config);

1.2 HttpApiConfig.FormatOptions

當序列化一個多屬性的模型時,FormatOptions能夠約束DateTime類型的屬性值轉換爲字符串的格式,也能夠指定屬性名是爲CamelCase。github

1.3 HttpApiConfig.HttpClient

首次獲取HttpClient實例時,HttpClient的實例將被建立,HttpClient屬性是一個IHttpClient接口,是對HttpClient對象的包裝,它的Handler暴露出與HttpClient關聯的HttpClientHandler對象。web

1.4 HttpApiConfig.GlobalFilters

GlobalFilters用於添加全局過濾器,這些過濾器不須要使用硬編碼修飾於接口,而是經過配置傳輸給接口的實例,適用於接口定義的項目和接口調用的項目分離開的項目結構。json

1.5 HttpApiConfig的生命週期

  • 在實例化HttpApiConfig以後,當再也不使用時,應該顯性地調用Dispose釋放資源;
  • 對於1.1的例子,若是myWebApi實現了IDisposable接口,調用myWebApi.Dispose()也會將HttpApiConfig的HttpClient屬性也釋放;
  • 對於var myWebApi = HttpApiClient.Create ()不傳入config的,內部將自動建立一個config實例,與myWebApi關聯,myWebApi.Dispose時,config實例也被釋放,但外部是獲取不到config實例的;

2.WebApiClient執行流程

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

3.使用自定義特性

WebApiClient內置不少特性,包含接口級、方法級、參數級的,他們分別是實現了IApiActionAttribute接口、IApiActionFilterAttribute接口、IApiParameterAttribute接口、IApiParameterable接口和IApiReturnAttribute接口的一個或多個接口。通常狀況下內置的特性就足以夠用,但實際項目中,你可能會遇到個別特殊的場景,須要本身實現一些特性或過濾器,主要用來操控請求上下文的RequestMessage對象,影響請求對象。框架

3.1 自定義IApiParameterAttribute例子

舉個例子:好比,服務端要求使用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);
    }
}

3.2 自定義過濾器

舉個例子:咱們須要爲每一個請求的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
{
    ...
}

3.3 自定義全局過濾器

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

4. DataAnnotations

在一些場景中,你的模型與服務須要的數據模塊可能不是所有吻合,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; }
}

5. 瞭解ITask對象

5.1 await ITask

await ITask,實際是調用了ITask.GetAwaiter()方法,等於同於ITask.InvokeAsync().GetAwaiter()方法。因此await ITask等同於await ITask.InvokeAsync()

5.2 ITask的InvokeAsync方法

InvokeAsync()返回Task對象,實際是http請求的任務對象。一個ITask實例,能夠屢次調用InvokeAsync()方法,完成屢次如出一轍的請求。ITask的不少擴展,是對InvokeAsync方法調用的包裝而獲得。

5.3 ITask的Retry和Handle

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次。

5.4 同步請求

HttpClient目前沒提供任何的同步請求方法,因此WebApiClient的請求也是同樣,若是遇到必須使用同步的場景,能夠暫時使用 ITask.GetAwaiter().GetResult()方法等待結果。

相關文章
相關標籤/搜索