.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()方法等待结果。

相关文章
相关标签/搜索