WebApiClient的netcoreapp版本,集高性能高可扩展性于一体的声明式http客户端库,特别适用于微服务的restful资源请求,也适用于各种畸形http接口请求。
<PackageReference Include="WebApiClientCore" Version="1.0.*" />
<PackageReference Include="WebApiClientCore.Extensions.OAuths" Version="1.0.*" />
进群注明WebApiClient
使用MockResponseHandler消除真实http请求,原生HttpClient、WebApiClientCore和Refit的性能参考:
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.836 (1903/May2019Update/19H1)
Intel Core i3-4150 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=3.1.202
[Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
DefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
Method | Mean | Error | StdDev |
---|---|---|---|
HttpClient_GetAsync | 3.146 μs | 0.0396 μs | 0.0370 μs |
WebApiClientCore_GetAsync | 12.421 μs | 0.2324 μs | 0.2174 μs |
Refit_GetAsync | 43.241 μs | 0.6713 μs | 0.6279 μs |
Method | Mean | Error | StdDev |
---|---|---|---|
HttpClient_PostJsonAsync | 5.263 μs | 0.0784 μs | 0.0733 μs |
WebApiClientCore_PostJsonAsync | 13.823 μs | 0.1874 μs | 0.1753 μs |
Refit_PostJsonAsync | 45.218 μs | 0.8166 μs | 0.7639 μs |
Method | Mean | Error | StdDev |
---|---|---|---|
WebApiClientCore_PutFormAsync | 21.14 μs | 0.407 μs | 0.418 μs |
Refit_PutFormAsync | 65.16 μs | 0.933 μs | 0.873 μs |
- 支持Task、Task<>和ITask<>三种异步返回
- 支持模型自动转换为Xml、Json、Form、和FormData共4种请求格式的内容
- 支持HttpResponseMessage、byte[]、string和Stream原生类型返回内容
- 支持原生HttpContent(比如StringContent)类型直接做为请求参数
- 内置丰富的能满足各种环境的常用特性(ActionAttribute和ParameterAttribute)
- 内置常用的FormDataFile等参数类型,同时支持自定义IApiParameter参数类型作为参数值
- 支持用户自定义IApiActionAttribute、IApiParameterAttribue、IApiReturnAttribute和IApiFilterAttribute
这个OpenApi文档在petstore.swagger.io,代码为使用WebApiClientCore.OpenApi.SourceGenerator工具将其OpenApi文档反向生成得到
/// <summary>
/// Everything about your Pets
/// </summary>
[LoggingFilter]
[HttpHost("https://petstore.swagger.io/v2/")]
public interface IPetApi : IHttpApi
{
/// <summary>
/// Add a new pet to the store
/// </summary>
/// <param name="body">Pet object that needs to be added to the store</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns></returns>
[HttpPost("pet")]
Task AddPetAsync([Required] [JsonContent] Pet body, CancellationToken cancellationToken = default);
/// <summary>
/// Update an existing pet
/// </summary>
/// <param name="body">Pet object that needs to be added to the store</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns></returns>
[HttpPut("pet")]
Task UpdatePetAsync([Required] [JsonContent] Pet body, CancellationToken cancellationToken = default);
/// <summary>
/// Finds Pets by status
/// </summary>
/// <param name="status">Status values that need to be considered for filter</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>successful operation</returns>
[HttpGet("pet/findByStatus")]
ITask<List<Pet>> FindPetsByStatusAsync([Required] IEnumerable<Anonymous> status, CancellationToken cancellationToken = default);
/// <summary>
/// Finds Pets by tags
/// </summary>
/// <param name="tags">Tags to filter by</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>successful operation</returns>
[Obsolete]
[HttpGet("pet/findByTags")]
ITask<List<Pet>> FindPetsByTagsAsync([Required] IEnumerable<string> tags, CancellationToken cancellationToken = default);
/// <summary>
/// Find pet by ID
/// </summary>
/// <param name="petId">ID of pet to return</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>successful operation</returns>
[HttpGet("pet/{petId}")]
ITask<Pet> GetPetByIdAsync([Required] long petId, CancellationToken cancellationToken = default);
/// <summary>
/// Updates a pet in the store with form data
/// </summary>
/// <param name="petId">ID of pet that needs to be updated</param>
/// <param name="name">Updated name of the pet</param>
/// <param name="status">Updated status of the pet</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns></returns>
[HttpPost("pet/{petId}")]
Task UpdatePetWithFormAsync([Required] long petId, [FormField] string name, [FormField] string status, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a pet
/// </summary>
/// <param name="api_key"></param>
/// <param name="petId">Pet id to delete</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns></returns>
[HttpDelete("pet/{petId}")]
Task DeletePetAsync([Header("api_key")] string api_key, [Required] long petId, CancellationToken cancellationToken = default);
/// <summary>
/// uploads an image
/// </summary>
/// <param name="petId">ID of pet to update</param>
/// <param name="additionalMetadata">Additional data to pass to server</param>
/// <param name="file">file to upload</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>successful operation</returns>
[HttpPost("pet/{petId}/uploadImage")]
ITask<ApiResponse> UploadFileAsync([Required] long petId, [FormDataText] string additionalMetadata, FormDataFile file, CancellationToken cancellationToken = default);
}
这个接口是在WebApiClientCore.Extensions.OAuths.IOAuthClient.cs代码中声明
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using WebApiClientCore.Attributes;
namespace WebApiClientCore.Extensions.OAuths
{
/// <summary>
/// 定义Token客户端的接口
/// </summary>
[LoggingFilter]
[XmlReturn(Enable = false)]
[JsonReturn(EnsureMatchAcceptContentType = false, EnsureSuccessStatusCode = false)]
public interface IOAuthClient
{
/// <summary>
/// 以client_credentials授权方式获取token
/// </summary>
/// <param name="endpoint">token请求地址</param>
/// <param name="credentials">身份信息</param>
/// <returns></returns>
[HttpPost]
[FormField("grant_type", "client_credentials")]
Task<TokenResult> RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] ClientCredentials credentials);
/// <summary>
/// 以password授权方式获取token
/// </summary>
/// <param name="endpoint">token请求地址</param>
/// <param name="credentials">身份信息</param>
/// <returns></returns>
[HttpPost]
[FormField("grant_type", "password")]
Task<TokenResult> RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] PasswordCredentials credentials);
/// <summary>
/// 刷新token
/// </summary>
/// <param name="endpoint">token请求地址</param>
/// <param name="credentials">身份信息</param>
/// <returns></returns>
[HttpPost]
[FormField("grant_type", "refresh_token")]
Task<TokenResult> RefreshTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] RefreshTokenCredentials credentials);
}
}
WebApiClientCore.Analyzers提供编码时语法分析与提示。
比如[Header]特性,可以声明在Interface、Method和Parameter三个地方,但是必须使用正确的构造器,否则运行时会抛出异常。有了语法分析功能,在声明接口时就不会使用不当的语法。如果想让语法分析生效,你的接口必须继承空方法的IHttpApi接口。
/// <summary>
/// 你的接口,记得要实现IHttpApi
/// </summary>
public interface IYourApi : IHttpApi
{
...
}
var services = new ServiceCollection();
services.AddHttpApi<IPetApi>(o =>
{
o.UseParameterPropertyValidate = true;
o.UseReturnValuePropertyValidate = false;
o.KeyValueSerializeOptions.IgnoreNullValues = true;
o.HttpHost = new Uri("http://localhost:6000/");
});
public class MyService
{
private readonly IpetApi petApi;
// 构造器注入IpetApi
public MyService(IpetApi petApi)
{
tihs.petApi = petApi;
}
}
每个接口的选项对应为HttpApiOptions<THttpApi>
,除了Action配置,我们也可以使用Configuration配置结合一起使用,这部分内容为Microsoft.Extensions.Options范畴。
services
.ConfigureHttpApi<IpetApi>(Configuration.GetSection(nameof(IpetApi)))
.ConfigureHttpApi<IpetApi>(o =>
{
// 符合国情的不标准时间格式,有些接口就是这么要求必须不标准
o.JsonSerializeOptions.Converters.Add(new JsonLocalDateTimeConverter("yyyy-MM-dd HH:mm:ss"));
});
{
"IpetApi": {
"HttpHost": "http://www.webappiclient.com/",
"UseParameterPropertyValidate": false,
"UseReturnValuePropertyValidate": false,
"JsonSerializeOptions": {
"IgnoreNullValues": true,
"WriteIndented": false
}
}
}
所有的Uri拼接都是通过Uri(Uri baseUri, Uri relativeUri)这个构造器生成。
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
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的结尾的第一种方式。
按照OpenApi,一个集合在Uri的Query或表单中支持5种表述方式,分别是:
- Csv // 逗号分隔
- Ssv // 空格分隔
- Tsv // 反斜杠分隔
- Pipes // 竖线分隔
- Multi // 多个同名键的键值对
对于 id = new string []{"001","002"} 这样的值,在PathQueryAttribute与FormContentAttribute处理后分别是:
CollectionFormat | Data |
---|---|
[PathQuery(CollectionFormat = CollectionFormat.Csv)] | id=001,002 |
[PathQuery(CollectionFormat = CollectionFormat.Ssv)] | id=001 002 |
[PathQuery(CollectionFormat = CollectionFormat.Tsv)] | id=001\002 |
[PathQuery(CollectionFormat = CollectionFormat.Pipes)] | `id=001 |
[PathQuery(CollectionFormat = CollectionFormat.Multi)] | id=001&id=002 |
每个接口都支持声明一个CancellationToken类型的参数,用于支持取消请求操作。CancellationToken.None表示永不取消,创建一个CancellationTokenSource,可以提供一个CancellationToken。
[HttpGet("api/users/{id}")]
ITask<User> GetAsync([Required]string id, CancellationToken token = default);
对于非表单的body内容,默认或缺省时的charset值,对应的是UTF8编码,可以根据服务器要求调整编码。
Attribute | ContentType |
---|---|
[JsonContent] | Content-Type: application/json; charset=utf-8 |
[JsonContent(CharSet ="utf-8")] | Content-Type: application/json; charset=utf-8 |
[JsonContent(CharSet ="unicode")] | Content-Type: application/json; charset=utf-16 |
这个用于控制客户端希望服务器返回什么样的内容格式,比如json或xml。
缺省配置是[JsonReturn(0.01),XmlReturn(0.01)],对应的请求accept值是
Accept: application/json; q=0.01, application/xml; q=0.01
在Interface或Method上显式地声明[JsonReturn]
,请求accept变为Accept: application/json, application/xml; q=0.01
在Interface或Method上声明[JsonReturn(Enable = false)]
,请求变为Accept: application/xml; q=0.01
在整个Interface或某个Method上声明[LoggingFilter]
,即可把请求和响应的内容输出到LoggingFactory中。如果要排除某个Method不打印日志,在该Method上声明[LoggingFilter(Enable = false)]
,即可将本Method排除。
[LoggingFilter]
public interface IUserApi : IHttpApi
{
[HttpGet("api/users/{account}")]
ITask<HttpResponseMessage> GetAsync([Required]string account);
// 禁用日志
[LoggingFilter(Enable =false)]
[HttpPost("api/users/body")]
Task<User> PostByJsonAsync([Required, JsonContent]User user, CancellationToken token = default);
}
class MyLogging : LoggingFilterAttribute
{
protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage)
{
xxlogger.Log(logMessage.ToIndentedString(spaceCount: 4));
return Task.CompletedTask;
}
}
使用ITask<>异步声明,就有Retry的扩展,Retry的条件可以为捕获到某种Exception或响应模型符合某种条件。
var result = await youApi.GetModelAsync(id: "id001")
.Retry(maxCount: 3)
.WhenCatch<HttpRequestException>()
.WhenResult(r => r.ErrorCode > 0);
请求一个接口,不管出现何种异常,最终都抛出HttpRequestException,HttpRequestException的内部异常为实际具体异常,之所以设计为内部异常,是为了完好的保存内部异常的堆栈信息。
WebApiClient内部的很多异常都基于ApiException这个抽象异常,也就是很多情况下,抛出的异常都是内为某个ApiException的HttpRequestException。
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)
{
// 异常
}
json patch是为客户端能够局部更新服务端已存在的资源而设计的一种标准交互,在RFC6902里有详细的介绍json patch,通俗来讲有以下几个要点:
- 使用HTTP PATCH请求方法;
- 请求body为描述多个opration的数据json内容;
- 请求的Content-Type为application/json-patch+json;
[HttpPatch("api/users/{id}")]
Task<UserInfo> PatchAsync(string id, JsonPatchDocument<User> doc);
var doc = new JsonPatchDocument<User>();
doc.Replace(item => item.Account, "laojiu");
doc.Replace(item => item.Email, "[email protected]");
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":"[email protected]"}]
配置CacheAttribute特性的Method会将本次的响应内容缓存起来,下一次如果符合预期条件的话,就不会再请求到远程服务器,而是从IResponseCacheProvider获取缓存内容,开发者可以自己实现ResponseCacheProvider。
// 缓存一分钟
[Cache(60 * 1000)]
[HttpGet("api/users/{account}")]
ITask<HttpResponseMessage> GetAsync([Required]string account);
public class RedisResponseCacheProvider : IResponseCacheProvider
{
public string Name => nameof(RedisResponseCacheProvider);
public Task<ResponseCacheResult> GetAsync(string key)
{
throw new NotImplementedException();
}
public Task SetAsync(string key, ResponseCacheEntry entry, TimeSpan expiration)
{
throw new NotImplementedException();
}
}
// 注册RedisResponseCacheProvider
var services = new ServiceCollection();
services.AddSingleton<IResponseCacheProvider, RedisResponseCacheProvider>();
有时候我们未必需要强模型,假设我们已经有原始的form文本内容,或原始的json文本内容,甚至是System.Net.Http.HttpContent对象,只需要把这些原始内请求到远程远程器。
[HttpPost]
Task PostAsync([RawStringContent("txt/plain")] string text);
[HttpPost]
Task PostAsync(StringContent text);
[HttpPost]
Task PostAsync([RawJsonContent] string json);
[HttpPost]
Task PostAsync([RawXmlContent] string xml);
[HttpPost]
Task PostAsync([RawFormContent] string form);
在某些极限情况下,比如人脸比对的接口,我们输入模型与传输模型未必是对等的:
{
"image1" : "图片1的base64",
"image2" : "图片2的base64"
}
class FaceModel
{
public Bitmap Image1 {get; set;}
public Bitmap Image2 {get; set;}
}
我们希望构造模型实例时传入Bitmap对象,但传输的时候变成Bitmap的base64值,所以我们要改造FaceModel,让它实现IApiParameter接口:
class FaceModel : IApiParameter
{
public Bitmap Image1 { get; set; }
public Bitmap Image2 { get; set; }
public Task OnRequestAsync(ApiParameterContext context)
{
var image1 = GetImageBase64(this.Image1);
var image2 = GetImageBase64(this.Image2);
var model = new { image1, image2 };
var jsonContent = new JsonContent();
context.HttpContext.RequestMessage.Content = jsonContent;
var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions;
var serializer = context.HttpContext.ServiceProvider.GetJsonSerializer();
serializer.Serialize(jsonContent, model, options);
}
private static string GetImageBase64(Bitmap image)
{
using var stream = new MemoryStream();
image.Save(stream, System.Drawing.Imaging.ImageFormat.Jpeg);
return Convert.ToBase64String(stream.ToArray());
}
}
最后,我们在使用改进后的FaceModel来请求
public interface IFaceApi
{
[HttpPost("/somePath")]
Task<HttpResponseMessage> PostAsync(FaceModel faces);
}
除了常见的xml或json响应内容要反序列化为强类型结果模型,你可能会遇到其它的二进制协议响应内容,比如google的ProtoBuf二进制内容。
public class ProtobufContentAttribute : HttpContentAttribute
{
public string ContentType { get; set; } = "application/x-protobuf";
protected override Task SetHttpContentAsync(ApiParameterContext context)
{
var stream = new MemoryStream();
if (context.ParameterValue != null)
{
Serializer.NonGeneric.Serialize(stream, context.ParameterValue);
stream.Position = 0L;
}
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue(this.ContentType);
context.HttpContext.RequestMessage.Content = content;
return Task.CompletedTask;
}
}
public class ProtobufReturnAttribute : ApiReturnAttribute
{
public ProtobufReturnAttribute(string acceptContentType = "application/x-protobuf")
: base(new MediaTypeWithQualityHeaderValue(acceptContentType))
{
}
public override async Task SetResultAsync(ApiResponseContext context)
{
if (context.ApiAction.Return.DataType.IsRawType == false)
{
var stream = await context.HttpContext.ResponseMessage.Content.ReadAsStreamAsync();
context.Result = Serializer.NonGeneric.Deserialize(context.ApiAction.Return.DataType.Type, stream);
}
}
}
[ProtobufReturn]
public interface IProtobufApi
{
[HttpPut("/users/{id}")]
Task<User> UpdateAsync([Required, PathQuery] string id, [ProtobufContent] User user);
}
在实际应用场景中,常常会遇到一些设计不标准的畸形接口,主要是早期还没有restful概念时期的接口,我们要区分分析这些接口,包装为好友的客户端调用接口。
例如服务器要求一个Query参数的名字为field-Name
,这个是c#关键字或变量命名不允许的,我们可以使用[AliasAsAttribute]
来达到这个要求:
[HttpGet("api/users/{account}")]
ITask<HttpResponseMessage> GetAsync([Required]string account, [AliasAs("field-Name")] string fieldName);
然后最终请求uri变为api/users/account1
?field-name=fileName1
字段 | 值 |
---|---|
field1 | someValue |
field2 | {"name":"sb","age":18} |
对应强类型模型是
class Model
{
public string Filed1 {get; set;}
public string Field2 {get; set;}
}
class Field2
{
public string Name {get; set;}
public int Age {get; set;}
}
我们在构建这个Model的实例时,不得不使用json序列化将field2的实例得到json文本,然后赋值给field2这个string属性。使用[JsonFormField]特性可以轻松帮我们自动完成Field2类型的json序列化并将结果字符串作为表单的一个字段。
public interface IMyApi
{
Task PostAsync([FormField] string field1, [JsonFormField] Field2 field2)
}
字段 | 值 |
---|---|
filed1 | someValue |
field2.name | sb |
field2.age | 18 |
其对应的json格式为
{
"field1" : "someValue",
"filed2" : {
"name" : "sb",
"age" : 18
}
}
合理情况下,对于复杂嵌套结构的数据模型,应当使用applicaiton/json,但接口要求必须使用Form提交,我可以配置KeyValueSerializeOptions来达到这个格式要求:
// 注册userApi
services.AddHttpApi<IUserApi>(o =>
{
o.KeyValueSerializeOptions.KeyNamingStyle = KeyNamingStyle.FullName;
});
明明响应的内容肉眼看上是json内容,但服务响应头里没有ContentType告诉客户端这内容是json,这好比客户端使用Form或json提交时就不在请求头告诉服务器内容格式是什么,而是让服务器猜测一样的道理。
解决办法是在Interface或Method声明[JsonReturn]
特性,并设置其EnsureMatchAcceptContentType属性为false,表示ContentType不是期望值匹配也要处理。
[JsonReturn(EnsureMatchAcceptContentType = false)]
public interface IJsonResponseApi : IHttpApi
{
}
例如每个请求的url额外的动态添加一个叫sign的参数,这个sign可能和请求参数值有关联,每次都需要计算。
我们可以自定义ApiFilterAttribute来实现自己的sign功能,然后把自定义Filter声明到Interface或Method即可
class SignFilterAttribute : ApiFilterAttribute
{
public override Task OnRequestAsync(ApiRequestContext context)
{
var sign = DateTime.Now.Ticks.ToString();
context.HttpContext.RequestMessage.AddUrlQuery("sign", sign);
return Task.CompletedTask;
}
}
[SignFilter]
public interface ISignedApi
{
...
}
services
.AddHttpApi<IMyApi>(o =>
{
o.HttpHost = new Uri("http://localhost:6000/");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
UseProxy = true,
Proxy = new WebProxy
{
Address = new Uri("http://proxy.com"),
Credentials = new NetworkCredential
{
UserName = "useranme",
Password = "pasword"
}
}
});
有些服务器为了限制客户端的连接,开启了https双向验证,只允许它执有它颁发的证书的客户端进行连接
services
.AddHttpApi<IMyApi>(o =>
{
o.HttpHost = new Uri("http://localhost:6000/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(yourCert);
return handler;
});
如果请求的接口不幸使用了Cookie保存身份信息机制,那么就要考虑维持CookieContainer实例不要跟随HttpMessageHandler的生命周期,默认的HttpMessageHandler最短只有2分钟的生命周期。
var cookieContainer = new CookieContainer();
services
.AddHttpApi<IMyApi>(o =>
{
o.HttpHost = new Uri("http://localhost:6000/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.CookieContainer = cookieContainer;
return handler;
});
使用WebApiClientCore.Extensions.OAuths扩展,轻松支持token的获取、刷新与应用
// 为接口注册与配置token提者选项
services.AddClientCredentialsTokenProvider<IMyApi>(o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
});
/// <summary>
/// 用户操作接口
/// </summary>
[ClientCredentialsToken]
public interface IMyApi
{
...
}
得到token值之后,怎么具体应用自己说了算
class MyTokenAttribute : ClientCredentialsTokenAttribute
{
protected override void UseTokenResult(ApiRequestContext context, TokenResult tokenResult)
{
context.HttpContext.RequestMessage.Headers.TryAddWithoutValidation("xxx-header", tokenResult.Access_token);
}
}
/// <summary>
/// 用户操作接口
/// </summary>
[MyToken]
public interface IMyApi
{
...
}
Microsoft.Extensions.Http支持收入各种第三方的HttpMessageHandler来build出一种安全的HttpClient,同时支持将此HttpClient实例包装为强类型服务的目标服务类型注册功能。
Microsoft.Extensions.Http.Polly项目依托于Polly,将Polly策略实现到System.Net.Http.DelegatingHandler,其handler可以为HttpClient提供重试、降级和断路等功能。
WebApiClientCore可以将Microsoft.Extensions.Http创建出来的HttpClient实例包装为声明式接口的代理实例,使开发者从面向命令式的编程模式直达声明式的AOP编程。