Skip to content

WebApiClient进阶

老九 edited this page Jun 1, 2018 · 13 revisions

1. 了解HttpApiConfig

HttpApiClient在创建接口动态代理类实例的时候,需要HttpApiConfig参数,默认不传就使用HttpApiConfig的默认实例,HttpApiConfig包含所有配置选项,包括Url配置、HttpClient配置、序列化工具配置、序列化格式配置、全局过滤器配置等。

var config = new HttpApiConfig
{
    // 请求的域名,会覆盖[HttpHost]特性
    HttpHost = new Uri("http://www.webapiclient.com"),
};
var client = HttpApiClient.Create<IMyWebApi>(config);

HttpApiConfig的生命周期与和它相关的HttpApiClient生命周期一致, 以上代码 ,client.Dispose()和config.Dispose()的结果是一样的。

2.WebApiClient执行流程

  • 1 创建接口实现类
    当调用WebApiClient.Create时,内部使用Emit创建接口的实现类,该实现类为接口的每个方法实现为:获取方法信息和调用参数值传给拦截器(IApiInterceptor)处理。

  • 2 拦截器创建ITask任务
    IApiInterceptor收到方法的调用时,根据方法信息和参数值创建Api描述对象ApiActionDescriptor,然后将和HttpApiConfig实例和ApiActionDescriptor包装成ITask任务对象并返回;

  • 3 等待调用者执行请求
    当调用者await ITask 或 await ITask.InvokeAsync()时,创建ApiActionContext并按照顺序执行ApiActionContext里描述的各种Attribute,这些Attribue影响着ApiActionContext的HttpRequestMessage等属性对象,然后使用HttpClient发送这个HttpRequestMessage对象,得到HttpResponseMessage,最后将HttpResponseMessage的Content转换为接口的返回值;

/// <summary>
/// 执行一次请求
/// </summary>
/// <returns></returns>
private async Task<TResult> RequestAsync()
{
    var context = new ApiActionContext
    {
        ApiActionDescriptor = this.apiActionDescriptor,
        HttpApiConfig = this.httpApiConfig,
        RequestMessage = new HttpApiRequestMessage { RequestUri = this.httpApiConfig.HttpHost },
        ResponseMessage = null,
        Exception = null,
        Result = null
    };

    await context.PrepareRequestAsync();
    await context.ExecFiltersAsync(filter => filter.OnBeginRequestAsync);

    var state = await context.ExecRequestAsync();
    await context.ExecFiltersAsync(filter => filter.OnEndRequestAsync);

    return state ? (TResult)context.Result : throw context.Exception;
}

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 IMyApi : IHttpApiClient
{
    ...
}

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 client = HttpApiClient.Create<IMyWebApi>(config);

3.4 自定义OAuth2全局过滤器

可以继承AuthTokenFilter这个抽象过滤过滤器,结合TokenClient实现符合自己的Token获取或自动刷新管理

/// <summary>
/// auth2 token全局过滤器
/// 用法:HttpApiConfig.GlobalFilters.Add( new TokenFilter("your client id","client secret") )
/// </summary>
class TokenFilter : AuthTokenFilter
{
    private readonly ITokenClient tokenClient = TokenClient.Get("http://localhost:5000/connect/token");

    /// <summary>
    /// 获取client_id
    /// </summary>
    public string ClientId { get; private set; }

    /// <summary>
    /// 获取client_secret
    /// </summary>
    public string ClientSecret { get; private set; }

    /// <summary>
    /// OAuth授权的token过滤器
    /// </summary>
    /// <param name="client_id">客户端id</param>
    /// <param name="client_secret">客户端密码</param>
    public TokenFilter(string client_id, string client_secret)
    {
        this.ClientId = client_id;
        this.ClientSecret = client_secret;
    }

    protected override async Task<TokenResult> RequestTokenResultAsync()
    {
        return await this.tokenClient.RequestClientCredentialsAsync(this.ClientId, this.ClientSecret);
    }

    protected override async Task<TokenResult> RequestRefreshTokenAsync(string refresh_token)
    {
        return await this.tokenClient.RequestRefreshTokenAsync(this.CliendId, this.ClientSecret, refresh_token);
    }
}

// 通过配置项将全局过滤器传给MyWebApi实例
var config = new HttpApiConfig();
config.GlobalFilters.Add(new TokenFilter ("client","secret"));
var client = HttpApiClient.Create<IMyWebApi>(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")]
    [IgnoreWhenNull] // 值为null则忽略序列化
    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对象

WebApiClient支持接口返回类型为Task<>和ITask<>两种类型,一建议使ITask<>而不是Task<>,因为后者比前者多一些扩展功能,比如Retry和Handle功能,这些功能都是比较实用的。ITask<>和Task<>一样支持await关键字,同时可以使用InvokeAsync()方法返回一个Task<>对象,但一般很少这么做。

5.1 ITask的Retry和Handle

Retry本质上是对ITask的InvokeAsync的包装,实际思想是当符合某种条件时,就多调用一次InvokeAsync方法,达到重试提交请求的目的。 Handle也是对ITask的InvokeAsync的包装,使用try catch对InvokeAsync方法封装为新的委托,当捕获到符合条件的异常类型时,就返回某种结果。

var result = await client.TestAsync()
    .Retry(3, i => TimeSpan.FromSeconds(i))
    .WhenCatch<Exception>()
    .HandleAsDefaultWhenException();

以上可以解读为,当遇到异常时,再重试请求,累计重试3次还是异常的话,处理为返回null值,期间总共最多请求了4次。

5.2 ITask的RX

在一些场景中,你可能不需要使用async/await异步编程方式,WebApiClient提供了Task对象转换为IObservable对象的扩展,使用方式如下:

var subscribe = client.TestAsync()
    .ToObservable()
    .Subscribe(Console.WriteLine,Console.WriteLine);
Clone this wiki locally