前言
我是WebApiClient庫的作者,目前在開發其
.netcore
版本,在整理其readme後,想想一來這部分内容可能對大家有用,二來興許能給WebApiClient帶人更多人氣,是以将readme作為部落格在此發表。
WebApiClientCore
WebApiClient.JIT的.netcore版本,基于HttpClient的高性能與高可擴充性于一體的聲明式Http用戶端庫,特别适用于微服務的restful資源請求,也适用于各種非标準的http接口請求。
PackageReference
<PackageReference Include="WebApiClientCore" Version="1.0.0-beta1" />
項目原因
- WebApiClient很優秀,它将不同架構不同平台都實作了統一的api
- WebApiClient不夠優秀,它在.netcore下完全可以更好,但它不得不相容.net45開始所有架構而有所犧牲
相對變化
- 使用
替換System.Text.Json
,提升序列化性能Json.net
- 移除HttpApiFactory和HttApiConfig功能,使用
Microsoft.Extensions.Http的HttpClientFactory
- 移除AOT功能,僅保留依賴于Emit的運作時代理
- 高效的ActionInvoker,對傳回Task<>和ITask<>作不同處理
- 所有特性都都變成中間件,基于管道編排各個特性并生成Action執行委托
- 良好設計的HttpContext、ApiRequestContext、ApiParameterContext和ApiResponseContext
Benchmark
WebApiClientCore、WebApiClient.JIT與原生HttpClient的性能比較,相比原生的HttpClient,WebApiClientCore幾乎沒有性能損耗。
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.778 (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 |
---|---|---|---|
WebApiClient_GetAsync | 279.479 us | 22.5466 us | 64.3268 us |
WebApiClientCore_GetAsync | 25.298 us | 0.4953 us | 0.7999 us |
HttpClient_GetAsync | 2.849 us | 0.0568 us | 0.1393 us |
WebApiClient_PostAsync | 25.942 us | 0.3817 us | 0.3188 us |
WebApiClientCore_PostAsync | 13.462 us | 0.2551 us | 0.6258 us |
HttpClient_PostAsync | 4.515 us | 0.0866 us | 0.0926 us |
聲明式接口定義
- 支援Task、Task<>和ITask<>三種異步傳回
- 支援模型自動轉換為Xml、Json、Form、和FormData共4種請求格式的内容
- 支援HttpResponseMessage、byte[]、string和Stream原生類型傳回内容
- 支援原生HttpContent(比如StringContent)類型直接做為請求參數
- 内置豐富的能滿足各種環境的常用特性(ActionAttribute和ParameterAttribute)
- 内置常用的FormDataFile等參數類型,同時支援自定義IApiParameter參數類型作為參數值
- 支援使用者自定義IApiActionAttribute、IApiParameterAttribue、IApiReturnAttribute和IApiFilterAttribute
以下聲明的代碼為使用 WebApiClientCore.Extensions.OpenApi
工具将openApi文檔反向生成得到
namespace Petstore
{
/// <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>
[HttpPost("pet")]
Task AddPetAsync([Required] [JsonContent] Pet body, CancellationToken token = default);
/// <summary>
/// Update an existing pet
/// </summary>
/// <param name="body">Pet object that needs to be added to the store</param>
[HttpPut("pet")]
Task<HttpResponseMessage> UpdatePetAsync([Required] [JsonContent] Pet body, CancellationToken token = default);
/// <summary>
/// Finds Pets by status
/// </summary>
/// <param name="status">Status values that need to be considered for filter</param>
/// <returns>successful operation</returns>
[HttpGet("pet/findByStatus")]
ITask<List<Pet>> FindPetsByStatusAsync([Required] IEnumerable<Anonymous> status);
/// <summary>
/// Finds Pets by tags
/// </summary>
/// <param name="tags">Tags to filter by</param>
/// <returns>successful operation</returns>
[Obsolete]
[HttpGet("pet/findByTags")]
ITask<List<Pet>> FindPetsByTagsAsync([Required] [PathQuery] IEnumerable<string> tags);
/// <summary>
/// Find pet by ID
/// </summary>
/// <param name="petId">ID of pet to return</param>
/// <returns>successful operation</returns>
[HttpGet("pet/{petId}")]
ITask<Pet> GetPetByIdAsync([Required] long petId);
/// <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>
[HttpPost("pet/{petId}")]
Task UpdatePetWithFormAsync([Required] long petId, [FormContent] string name, [FormContent] string status);
/// <summary>
/// Deletes a pet
/// </summary>
/// <param name="api_key"></param>
/// <param name="petId">Pet id to delete</param>
[HttpDelete("pet/{petId}")]
Task DeletePetAsync([Header("api_key")] string api_key, [Required] long petId);
/// <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>
/// <returns>successful operation</returns>
[LoggingFilter(Enable = false)]
[HttpPost("pet/{petId}/uploadImage")]
ITask<ApiResponse> UploadFileAsync([Required] long petId, [FormDataContent] string additionalMetadata, FormDataFile file);
}
}
另一個例子是 WebApiClientCore.Extensions.OAuths.IOAuthClient
接口聲明
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);
}
}
服務注冊與擷取
服務注冊
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;
}
}
請求和響應日志
在整個Interface或某個Method上聲明[LoggingFilter],即可把請求和響應的内容輸出到LoggingFactory中。
如果要排除某個Method不列印日志(比如大流量傳輸接口),在方法上聲明[LoggingFilter(Enable = false)],即可将本Method排除。
Accpet ContentType
這個用于控制用戶端希望伺服器傳回什麼樣的内容格式,比如json或xml,預設的配置值是Accept: application/json; q=0.01, application/xml; q=0.01
如果想json優先,可以在Interface或Method上聲明[JsonReturn],請求變為Accept: application/json, application/xml; q=0.01
如果想禁用其中一種,比如禁用xml,可以在Interface或Method上聲明[XmlReturn(Enable = false)],請求變為Accept: application/json; q=0.01
請求條件重試
使用ITask<>異步聲明,就有Retry的擴充,Retry的條件可以為捕獲到某種Exception或響應模型符合某種條件。
var result = await youApi.GetModelAsync(id: "id001")
.Retry(maxCount: 3)
.WhenCatch<Exception>()
.WhenResult(r => r.ErrorCode > 0);
OAuths&Token
使用
WebApiClientCore.Extensions.OAuths
擴充,輕松支援token的擷取、重新整理與應用
1 注冊相應類型的TokenProvider
// 為接口注冊與配置token提者選項
services.AddClientCredentialsTokenProvider<IpetApi>(o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
});
2 聲明對應的Token特性
/// <summary>
/// 使用者操作接口
/// </summary>
[ClientCredentialsToken]
public interface IpetApi
{
...
}
3 其它操作
清空Token,未過期的token也強制重新整理
var providers = serviceProvider.GetServices<ITokenProvider>();
foreach(var item in providers)
{
// 強制清除token以支援下次擷取到新的token
item.ClearToken();
}
自定義Token應用,得到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 IpetApi
{
...
}