WebApiClient
接口注册与选项
1 配置文件中配置HttpApiOptions选项
配置示例
"IUserApi": { "HttpHost": "http://www.webappiclient.com/", "UseParameterPropertyValidate": false, "UseReturnValuePropertyValidate": false, "JsonSerializeOptions": { "IgnoreNullValues": true, "WriteIndented": false } }
2 Service注册
示例
services .ConfigureHttpApi<IUserApi>(Configuration.GetSection(nameof(IUserApi))) .ConfigureHttpApi<IUserApi>(o => { // 符合国情的不标准时间格式,有些接口就是这么要求必须不标准 o.JsonSerializeOptions.Converters.Add(new JsonDateTimeConverter("yyyy-MM-dd HH:mm:ss")); });
HttpApiOptions详细展示
/// <summary> /// 表示HttpApi选项 /// </summary> public class HttpApiOptions { /// <summary> /// 获取或设置Http服务完整主机域名 /// 例如http://www.abc.com/或http://www.abc.com/path/ /// 设置了HttpHost值,HttpHostAttribute将失效 /// </summary> public Uri? HttpHost { get; set; } /// <summary> /// 获取或设置是否使用的日志功能 /// </summary> public bool UseLogging { get; set; } = true; /// <summary> /// 获取或设置请求头是否包含默认的UserAgent /// </summary> public bool UseDefaultUserAgent { get; set; } = true; /// <summary> /// 获取或设置是否对参数的属性值进行输入有效性验证 /// </summary> public bool . { get; set; } = true; /// <summary> /// 获取或设置是否对返回值的属性值进行输入有效性验证 /// </summary> public bool UseReturnValuePropertyValidate { get; set; } = true; /// <summary> /// 获取json序列化选项 /// </summary> public JsonSerializerOptions JsonSerializeOptions { get; } = CreateJsonSerializeOptions(); /// <summary> /// 获取json反序列化选项 /// </summary> public JsonSerializerOptions JsonDeserializeOptions { get; } = CreateJsonDeserializeOptions(); /// <summary> /// xml序列化选项 /// </summary> public XmlWriterSettings XmlSerializeOptions { get; } = new XmlWriterSettings(); /// <summary> /// xml反序列化选项 /// </summary> public XmlReaderSettings XmlDeserializeOptions { get; } = new XmlReaderSettings(); /// <summary> /// 获取keyValue序列化选项 /// </summary> public KeyValueSerializerOptions KeyValueSerializeOptions { get; } = new KeyValueSerializerOptions(); /// <summary> /// 获取自定义数据存储的字典 /// </summary> public Dictionary<object, object> Properties { get; } = new Dictionary<object, object>(); /// <summary> /// 获取接口的全局过滤器集合 /// </summary> public IList<IApiFilter> GlobalFilters { get; } = new List<IApiFilter>(); /// <summary> /// 创建序列化JsonSerializerOptions /// </summary> private static JsonSerializerOptions CreateJsonSerializeOptions() { return new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; } /// <summary> /// 创建反序列化JsonSerializerOptions /// </summary> /// <returns></returns> private static JsonSerializerOptions CreateJsonDeserializeOptions() { var options = CreateJsonSerializeOptions(); options.Converters.Add(JsonCompatibleConverter.EnumReader); options.Converters.Add(JsonCompatibleConverter.DateTimeReader); return options; } }
Uri(url)拼接规则
所有的Uri拼接都是通过Uri(Uri baseUri, Uri relativeUri)这个构造器生成。
带/
结尾的baseUri
-
http://a.com/
+b/c/d
=http://a.com/b/c/d
-
http://a.com/path1/
+b/c/d
=http://a.com/path1/b/c/d
-
http://a.com/path1/path2/
+b/c/d
=http://a.com/path1/path2/b/c/d
不带/
结尾的baseUri
-
http://a.com
+b/c/d
=http://a.com/b/c/d
-
http://a.com/path1
+b/c/d
=http://a.com/b/c/d
-
http://a.com/path1/path2
+b/c/d
=http://a.com/path1/b/c/d
事实上http://a.com
与http://a.com/
是完全一样的,他们的path都是/
,所以才会表现一样。为了避免低级错误的出现,请使用的标准baseUri书写方式,即使用/
作为baseUri的结尾的第一种方式。
OAuths&Token
推荐使用自定义TokenProvider
public class TestTokenProvider : TokenProvider { private readonly IConfiguration _configuration; public TestTokenProvider(IServiceProvider services,IConfiguration configuration) : base(services) { _configuration = configuration; } protected override Task<TokenResult> RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) { return this.RefreshTokenAsync(serviceProvider, refresh_token); } protected override async Task<TokenResult> RequestTokenAsync(IServiceProvider serviceProvider) { LoginInput login = new LoginInput(); login.UserNameOrEmailAddress = "admin"; login.Password = "bb123456"; var result = await serviceProvider.GetRequiredService<ITestApi>().RequestToken(login).Retry(maxCount: 3); return result; } }
TokenProvider的注册
services.AddTokenProvider<ITestApi,TestTokenProvider>();
OAuthTokenHandler
可以自定义OAuthTokenHandler官方定义是属于http消息处理器,功能与OAuthTokenAttribute一样,除此之外,如果因为意外的原因导致服务器仍然返回未授权(401状态码),其还会丢弃旧token,申请新token来重试一次请求。
OAuthToken在webapiclient中一般是保存在http请求的Header的Authrization
当token在url中时我们需要自定义OAuthTokenHandler
class UriQueryOAuthTokenHandler : OAuthTokenHandler { /// <summary> /// token应用的http消息处理程序 /// </summary> /// <param name="tokenProvider">token提供者</param> public UriQueryOAuthTokenHandler(ITokenProvider tokenProvider) : base(tokenProvider) { } /// <summary> /// 应用token /// </summary> /// <param name="request"></param> /// <param name="tokenResult"></param> protected override void UseTokenResult(HttpRequestMessage request, TokenResult tokenResult) { // var builder = new UriBuilder(request.RequestUri); // builder.Query += "mytoken=" + Uri.EscapeDataString(tokenResult.Access_token); // request.RequestUri = builder.Uri; var uriValue = new UriValue(request.RequestUri).AddQuery("myToken", tokenResult.Access_token); request.RequestUri = uriValue.ToUri(); } }
AddQuery是请求的的url中携带token的key
自定义OAuthTokenHandler的使用
services .AddHttpApi<IUserApi>() .AddOAuthTokenHandler((s, tp) => new UriQueryOAuthTokenHandler(tp)); //自定义TokoenProvider使用自定义OAuthTokenHandler apiBulider.AddOAuthTokenHandler<UrlTokenHandler>((sp,token)=> { token=sp.GetRequiredService<TestTokenProvider>(); return new UrlTokenHandler(token); },WebApiClientCore.Extensions.OAuths.TypeMatchMode.TypeOrBaseTypes);
OAuthToken 特性
OAuthToken可以定义在继承IHttpApi的接口上也可以定义在接口的方法上
在使用自定义TokenProvier时要注意OAuthToken特性不要定义在具有请求token的Http请求定义上
Patch请求
json patch是为客户端能够局部更新服务端已存在的资源而设计的一种标准交互,在RFC6902里有详细的介绍json patch,通俗来讲有以下几个要点:
- 使用HTTP PATCH请求方法;
- 请求body为描述多个opration的数据json内容;
- 请求的Content-Type为application/json-patch+json;
声明Patch方法
public interface IUserApi { [HttpPatch("api/users/{id}")] Task<UserInfo> PatchAsync(string id, JsonPatchDocument<User> doc); }
实例化JsonPatchDocument
var doc = new JsonPatchDocument<User>(); doc.Replace(item => item.Account, "laojiu"); doc.Replace(item => item.Email, "laojiu@qq.com");
请求内容
PATCH /api/users/id001 HTTP/1.1 Host: localhost:6000 User-Agent: WebApiClientCore/1.0.0.0 Accept: application/json; q=0.01, application/xml; q=0.01 Content-Type: application/json-patch+json [{"op":"replace","path":"/account","value":"laojiu"},{"op":"replace","path":"/email","value":"laojiu@qq.com"}]
异常处理
try { var model = await api.GetAsync(); } catch (HttpRequestException ex) when (ex.InnerException is ApiInvalidConfigException configException) { // 请求配置异常 } catch (HttpRequestException ex) when (ex.InnerException is ApiResponseStatusException statusException) { // 响应状态码异常 } catch (HttpRequestException ex) when (ex.InnerException is ApiException apiException) { // 抽象的api异常 } catch (HttpRequestException ex) when (ex.InnerException is SocketException socketException) { // socket连接层异常 } catch (HttpRequestException ex) { // 请求异常 } catch (Exception ex) { // 异常 }
请求重试
使用ITask<>异步声明,就有Retry的扩展,Retry的条件可以为捕获到某种Exception或响应模型符合某种条件。
GetNumberTemplateForEditOutput put = new GetNumberTemplateForEditOutput(); var res = await _testApi.GetForEdit(id).Retry(maxCount: 1).WhenCatchAsync<ApiResponseStatusException>(async p => { if (p.StatusCode == HttpStatusCode.Unauthorized) { await Token();//当http请求异常时报错,重新请求一次,保证token一直有效 } }); put = res.Result; return put;
API接口处理
使用ITask<>异步声明
[HttpHost("请求地址")]//请求地址域名 public interface ITestApi : IHttpApi { [OAuthToken]//权限 [JsonReturn]//设置返回格式 [HttpGet("/api/services/app/NumberingTemplate/GetForEdit")]//请求路径 ITask<AjaxResponse<GetNumberTemplateForEditOutput>> GetForEdit([Required] string id);//请求参数声明 [HttpPost("api/TokenAuth/Authenticate")] ITask<string> RequestToken([JsonContent] AuthenticateModel login); }
基于WebApiClient的扩展类
扩展类声明
/// <summary> /// WebApiClient扩展类 /// </summary> public static class WebApiClientExentions { public static IServiceCollection AddWebApiClietHttp<THttp>(this IServiceCollection services, Action<HttpApiOptions>? options = null) where THttp : class, IHttpApi { HttpApiOptions option = new HttpApiOptions(); option.JsonSerializeOptions.Converters.Add(new JsonDateTimeConverter("yyyy-MM-dd HH:mm:ss")); option.UseParameterPropertyValidate = true; if(options != null) { options.Invoke(option); } services.AddHttpApi<THttp>().ConfigureHttpApi(p => p = option); return services; } public static IServiceCollection AddWebApiClietHttp<THttp>(this IServiceCollection services,IConfiguration configuration) where THttp : class, IHttpApi { services.AddHttpApi<THttp>().ConfigureHttpApi((Microsoft.Extensions.Configuration.IConfiguration)configuration); return services; } public static IServiceCollection AddWebApiClientHttpWithTokeProvider<THttp, TTokenProvider>(this IServiceCollection services, Action<HttpApiOptions>? options = null) where THttp : class, IHttpApi where TTokenProvider : class, ITokenProvider { services.AddWebApiClietHttp<THttp>(options); services.AddTokenProvider<THttp,TTokenProvider>(); return services; } public static IServiceCollection AddWebApiClientHttpWithTokeProvider<THttp, TTokenProvider>(this IServiceCollection services, IConfiguration configuration) where THttp : class, IHttpApi where TTokenProvider : class, ITokenProvider { services.AddWebApiClietHttp<THttp>(configuration); services.AddTokenProvider<THttp, TTokenProvider>(); return services; } }
扩展类使用
services.AddWebApiClientHttpWithTokeProvider<ITestApi, TestTokenProvider>();