WebApiClientCore
WebApiClient.JIT/AOT的netcore版本,集高性能高可擴展性於一體的聲明式http客戶端庫,特別適用於微服務的restful資源請求,也適用於各種畸形http接口請求。
Nuget
如何使用
[HttpHost("http://localhost:5000/")]
public interface IUserApi
{
[HttpGet("api/users/{id}")]
Task<User> GetAsync(string id);
[HttpPost("api/users")]
Task<User> PostAsync([JsonContent] User user);
}
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpApi<IUserApi>();
}
public class MyService
{
private readonly IUserApi userApi;
public MyService(IUserApi userApi)
{
this.userApi = userApi;
}
}
QQ群協助
進群時請注明WebApiClient,在咨詢問題之前,請先認真閱讀以下剩余的文檔,避免消耗作者不必要的重復解答時間。
編譯時語法分析
WebApiClientCore.Analyzers提供編碼時語法分析與提示,聲明的接口繼承了空方法的IHttpApi接口,語法分析將生效,建議開發者開啟這個功能。
例如[Header]特性,可以聲明在Interface、Method和Parameter三個地方,但是必須使用正確的構造器,否則運行時會拋出異常。有了語法分析功能,在聲明接口時就不會使用不當的語法。
/// <summary>
/// 記得要實現IHttpApi
/// </summary>
public interface IUserApi : IHttpApi
{
...
}
接口配置與選項
每個接口的選項對應為HttpApiOptions
,選項名稱為接口的完整名稱,也可以通過HttpApi.GetName()方法獲取得到。
在IHttpClientBuilder配置
services
.AddHttpApi<IUserApi>()
.ConfigureHttpApi(Configuration.GetSection(nameof(IUserApi)))
.ConfigureHttpApi(o =>
{
// 符合國情的不標准時間格式,有些接口就是這么要求必須不標准
o.JsonSerializeOptions.Converters.Add(new JsonLocalDateTimeConverter("yyyy-MM-dd HH:mm:ss"));
});
配置文件的json
{
"IUserApi": {
"HttpHost": "http://www.webappiclient.com/",
"UseParameterPropertyValidate": false,
"UseReturnValuePropertyValidate": false,
"JsonSerializeOptions": {
"IgnoreNullValues": true,
"WriteIndented": false
}
}
}
在IServiceCollection配置
services
.ConfigureHttpApi<IUserApi>(Configuration.GetSection(nameof(IUserApi)))
.ConfigureHttpApi<IUserApi>(o =>
{
// 符合國情的不標准時間格式,有些接口就是這么要求必須不標准
o.JsonSerializeOptions.Converters.Add(new JsonLocalDateTimeConverter("yyyy-MM-dd HH:mm:ss"));
});
數據驗證
參數值驗證
對於參數值,支持ValidationAttribute特性修飾來驗證值。
public interface IUserApi
{
[HttpGet("api/users/{email}")]
Task<User> GetAsync([EmailAddress, Required] string email);
}
參數或返回模型屬性驗證
public interface IUserApi
{
[HttpPost("api/users")]
Task<User> PostAsync([Required][XmlContent] User user);
}
public class User
{
[Required]
[StringLength(10, MinimumLength = 1)]
public string Account { get; set; }
[Required]
[StringLength(10, MinimumLength = 1)]
public string Password { get; set; }
}
常用內置特性
內置特性指框架內提供的一些特性,拿來即用就能滿足一般情況下的各種應用。當然,開發者也可以在實際應用中,編寫滿足特定場景需求的特性,然后將自定義特性修飾到接口、方法或參數即可。
Return特性
特性名稱 | 功能描述 | 備注 |
---|---|---|
RawReturnAttribute | 處理原始類型返回值 | 缺省也生效 |
JsonReturnAttribute | 處理Json模型返回值 | 缺省也生效 |
XmlReturnAttribute | 處理Xml模型返回值 | 缺省也生效 |
常用Action特性
特性名稱 | 功能描述 | 備注 |
---|---|---|
HttpHostAttribute | 請求服務http絕對完整主機域名 | 優先級比Options配置低 |
HttpGetAttribute | 聲明Get請求方法與路徑 | 支持null、絕對或相對路徑 |
HttpPostAttribute | 聲明Post請求方法與路徑 | 支持null、絕對或相對路徑 |
HttpPutAttribute | 聲明Put請求方法與路徑 | 支持null、絕對或相對路徑 |
HttpDeleteAttribute | 聲明Delete請求方法與路徑 | 支持null、絕對或相對路徑 |
HeaderAttribute | 聲明請求頭 | 常量值 |
TimeoutAttribute | 聲明超時時間 | 常量值 |
FormFieldAttribute | 聲明Form表單字段與值 | 常量鍵和值 |
FormDataTextAttribute | 聲明FormData表單字段與值 | 常量鍵和值 |
常用Parameter特性
特性名稱 | 功能描述 | 備注 |
---|---|---|
PathQueryAttribute | 參數值的鍵值對作為url路徑參數或query參數的特性 | 缺省特性的參數默認為該特性 |
FormContentAttribute | 參數值的鍵值對作為x-www-form-urlencoded表單 | |
FormDataContentAttribute | 參數值的鍵值對作為multipart/form-data表單 | |
JsonContentAttribute | 參數值序列化為請求的json內容 | |
XmlContentAttribute | 參數值序列化為請求的xml內容 | |
UriAttribute | 參數值作為請求uri | 只能修飾第一個參數 |
ParameterAttribute | 聚合性的請求參數聲明 | 不支持細顆粒配置 |
HeaderAttribute | 參數值作為請求頭 | |
TimeoutAttribute | 參數值作為超時時間 | 值不能大於HttpClient的Timeout屬性 |
FormFieldAttribute | 參數值作為Form表單字段與值 | 只支持簡單類型參數 |
FormDataTextAttribute | 參數值作為FormData表單字段與值 | 只支持簡單類型參數 |
Filter特性
特性名稱 | 功能描述 | 備注 |
---|---|---|
ApiFilterAttribute | Filter特性抽象類 | |
LoggingFilterAttribute | 請求和響應內容的輸出為日志的過濾器 |
自解釋參數類型
類型名稱 | 功能描述 | 備注 |
---|---|---|
FormDataFile | form-data的一個文件項 | 無需特性修飾,等效於FileInfo類型 |
JsonPatchDocument | 表示將JsonPatch請求文檔 | 無需特性修飾 |
Uri拼接規則
所有的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的結尾的第一種方式。
表單集合處理
按照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|002 |
[PathQuery(CollectionFormat = CollectionFormat.Multi)] | id=001&id=002 |
CancellationToken參數
每個接口都支持聲明一個或多個CancellationToken類型的參數,用於支持取消請求操作。CancellationToken.None表示永不取消,創建一個CancellationTokenSource,可以提供一個CancellationToken。
[HttpGet("api/users/{id}")]
ITask<User> GetAsync([Required]string id, CancellationToken token = default);
ContentType CharSet
對於非表單的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 |
Accpet ContentType
這個用於控制客戶端希望服務器返回什么樣的內容格式,比如json或xml。
缺省配置值
缺省配置是[JsonReturn(0.01),XmlReturn(0.01)],對應的請求accept值是
Accept: application/json; q=0.01, application/xml; q=0.01
Json優先
在Interface或Method上顯式地聲明[JsonReturn]
,請求accept變為Accept: application/json, application/xml; q=0.01
禁用json
在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
{
[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 MyLoggingAttribute : LoggingFilterAttribute
{
protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage)
{
xxlogger.Log(logMessage.ToIndentedString(spaceCount: 4));
return Task.CompletedTask;
}
}
[MyLogging]
public interface IUserApi
{
}
原始類型返回值
當接口返回值聲明為如下類型時,我們稱之為原始類型,會被RawReturnAttribute處理。
返回類型 | 說明 |
---|---|
Task |
不關注響應消息 |
Task<HttpResponseMessage> |
原始響應消息類型 |
Task<Stream> |
原始響應流 |
Task<byte[]> |
原始響應二進制數據 |
Task<string> |
原始響應消息文本 |
接口聲明示例
Petstore接口
這個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);
}
IOAuthClient接口
這個接口是在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);
}
}
請求條件性重試
使用ITask<>異步聲明,就有Retry的擴展,Retry的條件可以為捕獲到某種Exception或響應模型符合某種條件。
public interface IUserApi
{
[HttpGet("api/users/{id}")]
ITask<User> GetAsync(string id);
}
var result = await userApi.GetAsync(id: "id001")
.Retry(maxCount: 3)
.WhenCatch<HttpRequestException>()
.WhenResult(r => r.Age <= 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)
{
// 異常
}
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"}]
響應內容緩存
配置CacheAttribute特性的Method會將本次的響應內容緩存起來,下一次如果符合預期條件的話,就不會再請求到遠程服務器,而是從IResponseCacheProvider獲取緩存內容,開發者可以自己實現ResponseCacheProvider。
聲明緩存特性
public interface IUserApi
{
// 緩存一分鍾
[Cache(60 * 1000)]
[HttpGet("api/users/{account}")]
ITask<HttpResponseMessage> GetAsync([Required]string account);
}
默認緩存條件:URL(如http://abc.com/a
)和指定的請求Header一致。
如果需要類似[CacheByPath]
這樣的功能,可直接繼承ApiCacheAttribute
來實現:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CacheByAbsolutePathAttribute : ApiCacheAttribute
{
public CacheByPathAttribute(double expiration) : base(expiration)
{
}
public override Task<string> GetCacheKeyAsync(ApiRequestContext context)
{
return Task.FromResult(context.HttpContext.RequestMessage.RequestUri.AbsolutePath);
}
}
自定義緩存提供者
默認的緩存提供者為內存緩存,如果希望將緩存保存到其它存儲位置,則需要自定義 緩存提者,並注冊替換默認的緩存提供者。
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);
原始json
[HttpPost]
Task PostAsync([RawJsonContent] string json);
原始xml
[HttpPost]
Task PostAsync([RawXmlContent] string xml);
原始表單內容
[HttpPost]
Task PostAsync([RawFormContent] string form);
自定義自解釋的參數類型
在某些極限情況下,比如人臉比對的接口,我們輸入模型與傳輸模型未必是對等的,例如:
服務端要求的json模型
{
"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二進制內容。
1 編寫相關自定義特性
自定義請求內容處理特性
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);
}
}
}
2 應用相關自定義特性
[ProtobufReturn]
public interface IProtobufApi
{
[HttpPut("/users/{id}")]
Task<User> UpdateAsync([Required, PathQuery] string id, [ProtobufContent] User user);
}
適配畸形接口
在實際應用場景中,常常會遇到一些設計不標准的畸形接口,主要是早期還沒有restful概念時期的接口,我們要區分分析這些接口,包裝為友好的客戶端調用接口。
不友好的參數名別名
例如服務器要求一個Query參數的名字為field-Name
,這個是c#關鍵字或變量命名不允許的,我們可以使用[AliasAsAttribute]
來達到這個要求:
public interface IDeformedApi
{
[HttpGet("api/users")]
ITask<string> GetAsync([AliasAs("field-Name")] string fieldName);
}
然后最終請求uri變為api/users/?field-name=fileNameValue
Form的某個字段為json文本
字段 | 值 |
---|---|
field1 | someValue |
field2 | {"name":"sb","age":18} |
對應強類型模型是
class Field2
{
public string Name {get; set;}
public int Age {get; set;}
}
常規下我們得把field2的實例json序列化得到json文本,然后賦值給field2這個string屬性,使用[JsonFormField]特性可以輕松幫我們自動完成Field2類型的json序列化並將結果字符串作為表單的一個字段。
public interface IDeformedApi
{
Task PostAsync([FormField] string field1, [JsonFormField] Field2 field2)
}
Form提交嵌套的模型
字段 | 值 |
---|---|
filed1 | someValue |
field2.name | sb |
field2.age | 18 |
其對應的json格式為
{
"field1" : "someValue",
"filed2" : {
"name" : "sb",
"age" : 18
}
}
合理情況下,對於復雜嵌套結構的數據模型,應當使用applicaiton/json,但接口要求必須使用Form提交,我可以配置KeyValueSerializeOptions來達到這個格式要求:
services.AddHttpApi<IDeformedApi>(o =>
{
o.KeyValueSerializeOptions.KeyNamingStyle = KeyNamingStyle.FullName;
});
響應未指明ContentType
明明響應的內容肉眼看上是json內容,但服務響應頭里沒有ContentType告訴客戶端這內容是json,這好比客戶端使用Form或json提交時就不在請求頭告訴服務器內容格式是什么,而是讓服務器猜測一樣的道理。
解決辦法是在Interface或Method聲明[JsonReturn]
特性,並設置其EnsureMatchAcceptContentType屬性為false,表示ContentType不是期望值匹配也要處理。
[JsonReturn(EnsureMatchAcceptContentType = false)]
public interface IDeformedApi
{
}
類簽名參數或apikey參數
例如每個請求的url額外的動態添加一個叫sign的參數,這個sign可能和請求參數值有關聯,每次都需要計算。
我們可以自定義ApiFilterAttribute來實現自己的sign功能,然后把自定義Filter聲明到Interface或Method即可
class SignFilterAttribute : ApiFilterAttribute
{
public override Task OnRequestAsync(ApiRequestContext context)
{
var signService = context.HttpContext.ServiceProvider.GetService<SignService>();
var sign = signService.SignValue(DateTime.Now);
context.HttpContext.RequestMessage.AddUrlQuery("sign", sign);
return Task.CompletedTask;
}
}
[SignFilter]
public interface IDeformedApi
{
...
}
HttpMessageHandler配置
Http代理配置
services
.AddHttpApi<IUserApi>(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<IUserApi>(o =>
{
o.HttpHost = new Uri("http://localhost:6000/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(yourCert);
return handler;
});
維持CookieContainer不變
如果請求的接口不幸使用了Cookie保存身份信息機制,那么就要考慮維持CookieContainer實例不要跟隨HttpMessageHandler的生命周期,默認的HttpMessageHandler最短只有2分鍾的生命周期。
var cookieContainer = new CookieContainer();
services
.AddHttpApi<IUserApi>(o =>
{
o.HttpHost = new Uri("http://localhost:6000/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.CookieContainer = cookieContainer;
return handler;
});
OAuths&Token
使用WebApiClientCore.Extensions.OAuths擴展,輕松支持token的獲取、刷新與應用。
對象與概念
對象 | 用途 |
---|---|
ITokenProviderFactory | tokenProvider的創建工廠,提供通過HttpApi接口類型創建tokenProvider |
ITokenProvider | token提供者,用於獲取token,在token的過期后的頭一次請求里觸發重新請求或刷新token |
OAuthTokenAttribute | token的應用特性,使用ITokenProviderFactory創建ITokenProvider,然后使用ITokenProvider獲取token,最后將token應用到請求消息中 |
OAuthTokenHandler | 屬於http消息處理器,功能與OAuthTokenAttribute一樣,除此之外,如果因為意外的原因導致服務器仍然返回未授權(401狀態碼),其還會丟棄舊token,申請新token來重試一次請求。 |
OAuth的Client模式
1 為接口注冊tokenProvider
// 為接口注冊與配置Client模式的tokenProvider
services.AddClientCredentialsTokenProvider<IUserApi>(o =>
{
o.Endpoint = new Uri("http://localhost:6000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
});
2 token的應用
2.1 使用OAuthToken特性
OAuthTokenAttribute屬於WebApiClientCore框架層,很容易操控請求內容和響應模型,比如將token作為表單字段添加到既有請求表單中,或者讀取響應消息反序列化之后對應的業務模型都非常方便,但它不能在請求內部實現重試請求的效果。在服務器頒發token之后,如果服務器的token丟失了,使用OAuthTokenAttribute會得到一次失敗的請求,本次失敗的請求無法避免。
/// <summary>
/// 用戶操作接口
/// </summary>
[OAuthToken]
public interface IUserApi
{
...
}
OAuthTokenAttribute默認實現將token放到Authorization請求頭,如果你的接口需要請token放到其它地方比如uri的query,需要重寫OAuthTokenAttribute:
class UriQueryTokenAttribute : OAuthTokenAttribute
{
protected override void UseTokenResult(ApiRequestContext context, TokenResult tokenResult)
{
context.HttpContext.RequestMessage.AddUrlQuery("mytoken", tokenResult.Access_token);
}
}
[UriQueryToken]
public interface IUserApi
{
...
}
2.1 使用OAuthTokenHandler
OAuthTokenHandler的強項是支持在一個請求內部里進行多次嘗試,在服務器頒發token之后,如果服務器的token丟失了,OAuthTokenHandler在收到401狀態碼之后,會在本請求內部丟棄和重新請求token,並使用新token重試請求,從而表現為一次正常的請求。但OAuthTokenHandler不屬於WebApiClientCore框架層的對象,在里面只能訪問原始的HttpRequestMessage與HttpResponseMessage,如果需要將token追加到HttpRequestMessage的Content里,這是非常困難的,同理,如果不是根據http狀態碼(401等)作為token無效的依據,而是使用HttpResponseMessage的Content對應的業務模型的某個標記字段,也是非常棘手的活。
// 注冊接口時添加OAuthTokenHandler
services
.AddHttpApi<IUserApi>()
.AddOAuthTokenHandler();
OAuthTokenHandler默認實現將token放到Authorization請求頭,如果你的接口需要請token放到其它地方比如uri的query,需要重寫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;
}
}
// 注冊接口時添加UriQueryOAuthTokenHandler
services
.AddHttpApi<IUserApi>()
.AddOAuthTokenHandler((s, tp) => new UriQueryOAuthTokenHandler(tp));
多接口共享的TokenProvider
可以給http接口設置基礎接口,然后為基礎接口配置TokenProvider,例如下面的xxx和yyy接口,都屬於IBaidu,只需要給IBaidu配置TokenProvider。
public interface IBaidu
{
}
[OAuthToken]
public interface IBaidu_XXX_Api : IBaidu
{
[HttpGet]
Task xxxAsync();
}
[OAuthToken]
public interface IBaidu_YYY_Api : IBaidu
{
[HttpGet]
Task yyyAsync();
}
// 注冊與配置password模式的token提者選項
services.AddPasswordCredentialsTokenProvider<IBaidu>(o =>
{
o.Endpoint = new Uri("http://localhost:5000/api/tokens");
o.Credentials.Client_id = "clientId";
o.Credentials.Client_secret = "xxyyzz";
o.Credentials.Username = "username";
o.Credentials.Password = "password";
});
自定義TokenProvider
擴展包已經內置了OAuth的Client和Password模式兩種標准token請求,但是仍然還有很多接口提供方在實現上僅僅體現了它的精神,這時候就需要自定義TokenProvider,假設接口提供方的獲取token的接口如下:
public interface ITokenApi
{
[HttpPost("http://xxx.com/token")]
Task<TokenResult> RequestTokenAsync([Parameter(Kind.Form)] string clientId, [Parameter(Kind.Form)] string clientSecret);
}
委托TokenProvider
委托TokenProvider是一種最簡單的實現方式,它將請求token的委托作為自定義TokenProvider的實現邏輯:
// 為接口注冊自定義tokenProvider
services.AddTokeProvider<IUserApi>(s =>
{
return s.GetService<ITokenApi>().RequestTokenAsync("id", "secret");
});
完整實現的TokenProvider
// 為接口注冊CustomTokenProvider
services.AddTokeProvider<IUserApi, CustomTokenProvider>();
class CustomTokenProvider : TokenProvider
{
public CustomTokenProvider(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
protected override Task<TokenResult> RequestTokenAsync(IServiceProvider serviceProvider)
{
return serviceProvider.GetService<ITokenApi>().RequestTokenAsync("id", "secret");
}
protected override Task<TokenResult> RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token)
{
return this.RequestTokenAsync(serviceProvider);
}
}
自定義TokenProvider的選項
每個TokenProvider都有一個Name屬性,與service.AddTokeProvider()返回的ITokenProviderBuilder的Name是同一個值。讀取Options值可以使用TokenProvider的GetOptionsValue()方法,配置Options則通過ITokenProviderBuilder的Name來配置。
NewtonsoftJson處理json
不可否認,System.Text.Json由於性能的優勢,會越來越得到廣泛使用,但NewtonsoftJson也不會因此而退出舞台。
System.Text.Json在默認情況下十分嚴格,避免代表調用方進行任何猜測或解釋,強調確定性行為,該庫是為了實現性能和安全性而特意這樣設計的。Newtonsoft.Json默認情況下十分靈活,默認的配置下,你幾乎不會遇到反序列化的種種問題,雖然這些問題很多情況下是由於不嚴謹的json結構或類型聲明造成的。
擴展包
默認的基礎包是不包含NewtonsoftJson功能的,需要額外引用WebApiClientCore.Extensions.NewtonsoftJson這個擴展包。
配置[可選]
// ConfigureNewtonsoftJson
services.AddHttpApi<IUserApi>().ConfigureNewtonsoftJson(o =>
{
o.JsonSerializeOptions.NullValueHandling = NullValueHandling.Ignore;
});
聲明特性
使用[JsonNetReturn]替換內置的[JsonReturn],[JsonNetContent]替換內置[JsonContent]
/// <summary>
/// 用戶操作接口
/// </summary>
[JsonNetReturn]
public interface IUserApi
{
[HttpPost("/users")]
Task PostAsync([JsonNetContent] User user);
}
JsonRpc調用
在極少數場景中,開發者可能遇到JsonRpc調用的接口,由於該協議不是很流行,WebApiClientCore將該功能的支持作為WebApiClientCore.Extensions.JsonRpc擴展包提供。使用[JsonRpcMethod]修飾Rpc方法,使用[JsonRpcParam]修飾Rpc參數
即可。
JsonRpc聲明
[HttpHost("http://localhost:5000/jsonrpc")]
public interface IUserApi
{
[JsonRpcMethod("add")]
ITask<JsonRpcResult<User>> AddAsync([JsonRpcParam] string name, [JsonRpcParam] int age, CancellationToken token = default);
}
JsonRpc數據包
POST /jsonrpc HTTP/1.1
Host: localhost:5000
User-Agent: WebApiClientCore/1.0.6.0
Accept: application/json; q=0.01, application/xml; q=0.01
Content-Type: application/json-rpc
{"jsonrpc":"2.0","method":"add","params":["laojiu",18],"id":1}