WebApiClientCore
WebApiClient.JIT/AOT的.NET Core 版本,集高性能高可扩展性于一体的声明式 http 客户端库,特别适用于微服务的 restful 资源请求,也适用于各种畸形 http 接口请求。
NuGet
how to use
[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 group assistance
Please indicate ** WebApiClient ** when joining the group. Before asking questions, please read the remaining documents below carefully to avoid consuming unnecessary repeated answer time for the author.
compile-time parsing
WebApiClientCore.Analyzers provides syntax analysis and prompts during encoding. The declared interface inherits the IHttpApi interface of empty methods, and syntax analysis will take effect. It is recommended that developers turn on this feature.
For example, the [Header] attribute can be declared in three places: Interface, Method, and Parameter, but the correct constructor must be used, otherwise an exception will be thrown at runtime. With parsing, improper syntax is not used when declaring interfaces.
/// <summary>
/// 记得要实现IHttpApi
/// </summary>
public interface IUserApi : IHttpApi
{
...
}
Interface configuration and options
每个接口的选项对应为HttpApiOptions,选项名称为接口的完整名称,也可以通过 HttpApi.GetName()方法获取得到。
Configure in IHttpClientBuilder
services
.AddHttpApi<IUserApi>()
.ConfigureHttpApi(Configuration.GetSection(nameof(IUserApi)))
.ConfigureHttpApi(o =>
{
// 符合国情的不标准时间格式,有些接口就是这么要求必须不标准
o.JsonSerializeOptions.Converters.Add(new JsonLocalDateTimeConverter("yyyy-MM-dd HH:mm:ss"));
});
Configuration file json
{
"IUserApi": {
"HttpHost": "http://www.webappiclient.com/",
"UseParameterPropertyValidate": false,
"UseReturnValuePropertyValidate": false,
"JsonSerializeOptions": {
"IgnoreNullValues": true,
"WriteIndented": false
}
}
}
Configure in IServiceCollection
services
.ConfigureHttpApi<IUserApi>(Configuration.GetSection(nameof(IUserApi)))
.ConfigureHttpApi<IUserApi>(o =>
{
// 符合国情的不标准时间格式,有些接口就是这么要求必须不标准
o.JsonSerializeOptions.Converters.Add(new JsonLocalDateTimeConverter("yyyy-MM-dd HH:mm:ss"));
});
data validation
Parameter value verification
For parameter values, the ValidationAttribute attribute modification is supported to verify the value.
public interface IUserApi
{
[HttpGet("api/users/{email}")]
Task<User> GetAsync([EmailAddress, Required] string email);
}
Parameter or return model attribute verification
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; }
}
Common built-in features
Built-in features refer to some features provided within the framework that can be used immediately to meet various applications in general. Of course, developers can also write features that meet the needs of specific scenarios in practical applications, and then modify custom features into interfaces, methods, or parameters.
Return characteristic
| property name | functional description | remarks |
|---|---|---|
| RawReturnAttribute | Handling original type return values | Default also takes effect |
| JsonReturnAttribute | Processing Json model return values | Default also takes effect |
| XmlReturnAttribute | Processing Xml model return values | Default also takes effect |
Common Action Features
| property name | functional description | remarks |
|---|---|---|
| HttpHostAttribute | Request service http absolute complete hosting domain name | Lower priority than Options configuration |
| HttpGetAttribute | Declare the Get request method and path | Supports null, absolute or relative paths |
| HttpPostAttribute | Declare Post Request Method and Path | Supports null, absolute or relative paths |
| HttpPutAttribute | Declare the Put request method and path | Supports null, absolute or relative paths |
| HttpDeleteAttribute | Declare the Delete request method and path | Supports null, absolute or relative paths |
| HeaderAttribute | declaration request header | constant value |
| TimeoutAttribute | Claim timeout | constant value |
| FormFieldAttribute | Declare Form form fields and values | Constant keys and values |
| FormDataTextAttribute | Declare FormData form fields and values | Constant keys and values |
Common Parameter Characteristics
| property name | functional description | remarks |
|---|---|---|
| PathQueryAttribute | Characteristics of key-value pairs of parameter values as url path parameters or query parameters | The parameters of the default feature default to this feature |
| FormContentAttribute | Key-value pairs of parameter values are used as an x-www-form-urlencoded form | |
| FormDataContentAttribute | Key-value pairs of parameter values serve as a multipart/form-data form | |
| JsonContentAttribute | Parameter values are serialized to the requested json content | |
| XmlContentAttribute | Parameter values are serialized into the requested xml content | |
| UriAttribute | Parameter value as request uri | Only the first parameter can be modified |
| ParameterAttribute | Aggregate request parameter declaration | Fine-grain configuration is not supported |
| HeaderAttribute | Parameter value as request header | |
| TimeoutAttribute | Parameter value as timeout | Value cannot be greater than the Timeout property of HttpClient |
| FormFieldAttribute | Parameter values as Form form fields and values | Only simple type parameters are supported |
| FormDataTextAttribute | Parameter values as FormData form fields and values | Only simple type parameters are supported |
Filter feature
| property name | functional description | remarks |
|---|---|---|
| ApiFilterAttribute | Filter attribute abstract class | |
| LoggingFilterAttribute | The output of request and response content is a filter for the log |
Self-explanatory parameter types
| type name | functional description | remarks |
|---|---|---|
| FormDataFile | A file entry for form-data | No feature modification, equivalent to FileInfo type |
| JsonPatchDocument | Indicates that JsonPatch will request documents | No feature modification required |
Uri splicing rule
All Uri splicing are generated using the constructor Uri (Uri base Uri, Uri relativeUri).
带/结尾的 baseUri
http://a.com/+b/c/d=http://a.com/b/c/dhttp://a.com/path1/+b/c/d=http://a.com/path1/b/c/dhttp://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/dhttp://a.com/path1+b/c/d=http://a.com/b/c/dhttp://a.com/path1/path2+b/c/d=http://a.com/path1/b/c/d
事实上http://a.com与http://a.com/是完全一样的,他们的 path 都是/,所以才会表现一样。为了避免低级错误的出现,请使用的标准 baseUri 书写方式,即使用/作为 baseUri 的结尾的第一种方式。
Form collection processing
According to OpenApi, a collection supports five representations in Uri's Query or Form, which are:
- Csv//comma separated
- Ssv//space separation
- Tsv//backslash separation
- Pipes//Vertical line separation
- Multi//Key-value pairs with multiple keys with the same name
For values such as id = new string []{"001 "," 002 "}, after processing with PathQueryAttribute and FormContentAttribute, they are:
| 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 parameter
Each interface supports declaring one or more parameters of type CancellationToken to support cancellation of the request. CancellationToken. None means never cancel. Create a CancellationTokenSource, which can provide a CancellationToken.
[HttpGet("api/users/{id}")]
ITask<User> GetAsync([Required]string id, CancellationToken token = default);
ContentType CharSet
For non-form body content, the default or default charset value corresponds to UTF8 encoding, and the encoding can be adjusted according to server requirements.
| 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
This controls what content format the client wants the server to return, such as json or xml.
default configuration values
The default configuration is [JsonReturn (0.01), XmlReturn (0.01)], and the corresponding request accept value is
Accept: application/json; q=0.01, application/xml; q=0.01
Json first
在 Interface 或 Method 上显式地声明[JsonReturn],请求 accept 变为Accept: application/json, application/xml; q=0.01
Disable json
在 Interface 或 Method 上声明[JsonReturn(Enable = false)],请求变为Accept: application/xml; q=0.01
Request and response logs
在整个 Interface 或某个 Method 上声明[LoggingFilter],即可把请求和响应的内容输出到 LoggingFactory 中。如果要排除某个 Method 不打印日志,在该 Method 上声明[LoggingFilter(Enable = false)],即可将本 Method 排除。
default log
[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);
}
Customize log output targets
class MyLoggingAttribute : LoggingFilterAttribute
{
protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage)
{
xxlogger.Log(logMessage.ToIndentedString(spaceCount: 4));
return Task.CompletedTask;
}
}
[MyLogging]
public interface IUserApi
{
}
Original type return value
When the interface return value is declared as the following type, we call it the primitive type and will be processed by the RawReturnAttribute.
| return type | description |
|---|---|
Task |
Don't pay attention to response messages |
Task<HttpResponseMessage> |
Original response message type |
Task<Stream> |
raw response stream |
Task<byte[]> |
Raw response binary data |
Task<string> |
Original response message text |
Example interface declaration
Petstore interface
这个 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 interface
This interface is declared in the WebApiClientCore.Extensions.OAuths.IOAuthClient.cs code
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);
}
}
Request conditional retry
With ITask <> asynchronous declarations, there is an extension to Retry, which can be conditional on catching an Exception or on the response model meeting certain conditions.
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);
Exceptions and exception handling
Request an interface, no matter what exception occurs, HttpRequestException will eventually be thrown. The inner exception of HttpRequestException is an actual specific exception. The reason why it is designed as an inner exception is to perfectly preserve the stack information of the inner exception.
Many exceptions within WebApiClient are based on the abstract exception ApiException, which means that in many cases, the exceptions thrown are HttpRequestException with an ApiException built into it.
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 request
json patch is a standard interaction designed for clients to partially update existing resources on the server. json patch is described in detail in RFC 6902. In general terms, it has the following key points:
- Use the HTTP PATCH request method;
- The request body is the data json content describing multiple motions;
- The Content-Type of the request is application/json-patch + json;
Declare the Patch Method
public interface IUserApi
{
[HttpPatch("api/users/{id}")]
Task<UserInfo> PatchAsync(string id, JsonPatchDocument<User> doc);
}
instantiate a JsonPatchDocument
var doc = new JsonPatchDocument<User>();
doc.Replace(item => item.Account, "laojiu");
doc.Replace(item => item.Email, "laojiu@qq.com");
requested content
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"}]
Response content caching
The method for configuring the CacheAttribute feature will cache the response content this time. If the expected conditions are met next time, the request will not be made to the remote server. Instead, the cached content will be obtained from the IResponseCacheProvider. Developers can implement the ResponseCacheProvider themselves.
Declare caching characteristics
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);
}
}
Custom cache providers
The default cache provider is memory cache. If you want to save the cache to another storage location, you need to customize the cache lifter and register to replace the default cache provider.
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>();
non-model request
Sometimes we don't necessarily need a strong model. Suppose we already have original form text content, or original json text content, or even a System. Net. Http.HttpContent object, we just need to request these original text content to the remote server.
original text
[HttpPost]
Task PostAsync([RawStringContent("txt/plain")] string text);
[HttpPost]
Task PostAsync(StringContent text);
Original json
[HttpPost]
Task PostAsync([RawJsonContent] string json);
original XML
[HttpPost]
Task PostAsync([RawXmlContent] string xml);
Original form content
[HttpPost]
Task PostAsync([RawFormContent] string form);
Custom and self-interpreted parameter types
In some extreme cases, such as the interface for face comparison, our input model and transmission model may not be equivalent, for example:
-
- json model required by the server **
{
"image1": "图片1的base64",
"image2": "图片2的base64"
}
-
- The business model expected by the client **
class FaceModel
{
public Bitmap Image1 {get; set;}
public Bitmap Image2 {get; set;}
}
We want to pass in a Bitmap object when constructing a model instance, but it becomes the base64 value of Bitmap when transferring it, so we need to transform FaceModel to implement the IApiParameter interface:
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());
}
}
Finally, we are using the improved FaceModel to request
public interface IFaceApi
{
[HttpPost("/somePath")]
Task<HttpResponseMessage> PostAsync(FaceModel faces);
}
Analyze customized request content and response content
In addition to the common xml or json response content that needs to be deserialized into a strongly typed result model, you may encounter other binary protocol response content, such as Google's ProtoBuf binary content.
1 Write relevant custom features
Customize request content processing features
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;
}
}
Custom response content parsing features
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 Application-related customization features
[ProtobufReturn]
public interface IProtobufApi
{
[HttpPut("/users/{id}")]
Task<User> UpdateAsync([Required, PathQuery] string id, [ProtobufContent] User user);
}
malformed interface
In actual application scenarios, we often encounter some malformed interfaces with non-standard designs, mainly interfaces in the early period when there was no restful concept. We need to distinguish and analyze these interfaces and wrap them into friendly client-side invocation interfaces.
Unfriendly parameter name aliases
例如服务器要求一个 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
A field of the Form is json text
| field | value |
|---|---|
| field1 | someValue |
| field2 |
The corresponding strongly typed model is
class Field2
{
public string Name {get; set;}
public int Age {get; set;}
}
Normally, we have to serialize the json instance of field2 to get the json text, and then assign it to the string attribute of field2. Using the [JsonFormField] feature can easily help us automatically complete the serialization of field2 type json and use the result string as a field of the form.
public interface IDeformedApi
{
Task PostAsync([FormField] string field1, [JsonFormField] Field2 field2)
}
Form submits nested models
| field | value |
|---|---|
| filed1 | someValue |
| field2.name | sb |
| field2.age | 18 |
Its corresponding json format is
{
"field1": "someValue",
"filed2": {
"name": "sb",
"age": 18
}
}
Under reasonable circumstances, applicaiton/json should be used for complex nested data models, but the interface requirements must be submitted using Form. I can configure KeyValueSerializeOptions to achieve this format requirement:
services.AddHttpApi <
IDeformedApi >
((o) => {
o.KeyValueSerializeOptions.KeyNamingStyle = KeyNamingStyle.FullName;
});
The response did not specify a ContentType
The content of the response clearly appears to be json content to the naked eye, but there is no ContentType in the service response header to tell the client that the content is json. This is like when the client uses Form or json to submit, it does not tell the server what the content format is in the request header, but lets the server guess.
解决办法是在 Interface 或 Method 声明[JsonReturn]特性,并设置其 EnsureMatchAcceptContentType 属性为 false,表示 ContentType 不是期望值匹配也要处理。
[JsonReturn(EnsureMatchAcceptContentType = false)]
public interface IDeformedApi
{
}
Class signature parameter or apikey parameter
For example, an additional parameter called sign is dynamically added to the url of each request. This sign may be related to the value of the request parameter and needs to be calculated every time.
We can customize the ApiFilterAttribute to implement our own sign function, and then declare the custom Filter to the Interface or 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 Configuration
Http proxy configuration
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"
}
}
});
Client certificate configuration
In order to restrict client connections, some servers turn on https two-way authentication, allowing only clients with certificates it issued to connect
services
.AddHttpApi<IUserApi>(o =>
{
o.HttpHost = new Uri("http://localhost:6000/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(yourCert);
return handler;
});
Keep CookieContainer unchanged
If the requested interface unfortunately uses a cookie to store identity information, consider maintaining the CookieContainer instance without following the life cycle of HttpMessageHandler. The default HttpMessageHandler has a minimum life cycle of 2 minutes.
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
Use the WebApiClientCore.Extensions.OAuths extension to easily support the acquisition, refresh and application of tokens.
Objects and concepts
| object | use |
|---|---|
| ITokenProviderFactory | The creation factory of tokenProvider, which provides creating tokenProvider through the HttpApi interface type |
| ITokenProvider | token provider, used to obtain a token, and trigger a re-request or refresh of the token in the first request after the token expires |
| OAuthTokenAttribute | The application features of tokens are used to create an ITokenProvider using ITokenProviderFactory, then use the ITokenProvider to obtain the token, and finally apply the token to the request message |
| OAuthTokenHandler | It belongs to an http message processor and has the same function as the OAuthTokenAttribute. In addition, if the server still returns unauthorized (401 status code) due to unexpected reasons, it will also discard the old token and apply for a new token to retry the request. |
OAuth's Client Mode
1 Register a tokenProvider for an interface
// 为接口注册与配置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";
});
Application of 2 tokens
2.1 Using OAuthToken features
OAuthTokenAttribute belongs to the WebApiClientCore framework layer, which makes it easy to manipulate the request content and response model. For example, it is very convenient to add a token as a form field to an existing request form, or to read the corresponding business model after the response message is deserialized. However, it cannot achieve the effect of retrying the request within the request. After the server issues a token, if the server's token is lost, using OAuthTokenAttribute will get a failed request, and this failed request cannot be avoided.
/// <summary>
/// 用户操作接口
/// </summary>
[OAuthToken]
public interface IUserApi
{
...
}
The default implementation of OAuthTokenAttribute puts the token in the Authorization request header. If your interface needs to ask the token to be placed elsewhere, such as the uri query, you need to override the 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 Using OAuthTokenHandler
The strength of OAuthTokenHandler is that it supports multiple attempts within a request. After the server issues a token, if the server's token is lost, OAuthTokenHandler will discard and re-request the token within the request after receiving the 401 status code, and retry the request with the new token, thus appearing as a normal request. However, OAuthTokenHandler is not an object in the WebApiClientCore framework layer. It can only access the original HttpRequestMessage and HttpResponseMessage. If you need to add a token to the Content of HttpRequestMessage, this is very difficult. Similarly, if you don't use the http status code (401, etc.) as the basis for invalidating the token, but use a certain token field of the business model corresponding to the Content of HttpResponseMessage, it is also very difficult to do.
// 注册接口时添加OAuthTokenHandler
services
.AddHttpApi<IUserApi>()
.AddOAuthTokenHandler();
By default, OAuthTokenHandler implements putting the token in the Authorization request header. If your interface needs to ask the token to be placed elsewhere, such as the uri query, you need to override 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 shared by multiple interfaces
You can set a basic interface for the http interface, and then configure TokenProvider for the basic interface. For example, the following xxx and yyy interfaces both belong to IBaidu. You only need to configure TokenProvider for IBaidu.
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";
});
Customize TokenProvider
The extension package already has two standard token requests in OAuth's Client and Password mode, but there are still many interface providers that only reflect its spirit in implementation. At this time, you need to customize TokenProvider. Assuming that the interface provider's interface to obtain tokens is as follows:
public interface ITokenApi
{
[HttpPost("http://xxx.com/token")]
Task<TokenResult> RequestTokenAsync([Parameter(Kind.Form)] string clientId, [Parameter(Kind.Form)] string clientSecret);
}
Delegate TokenProvider
Delegate TokenProvider is the simplest implementation method. It uses the delegate requesting token as the implementation logic of a custom TokenProvider:
// 为接口注册自定义tokenProvider
services.AddTokeProvider<IUserApi>(s =>
{
return s.GetService<ITokenApi>().RequestTokenAsync("id", "secret");
});
Fully implemented 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);
}
}
Options for customizing TokenProvider
Each TokenProvider has a Name attribute, which is the same value as the Name of the ITokenProviderBuilder returned by service.AddTokeProvider(). You can use TokenProvider's GetOptionsValue() method to read Options values, and configure Options through ITokenProviderBuilder's Name.
NewtonsoftJson handles json
There is no denying that System.Text.Json will become more and more widely used due to its performance advantages, but NewtonsoftJson will not retire from the stage because of this.
System.Text.Json is strict by default, avoiding any guesses or interpretations on behalf of callers and emphasizing deterministic behavior, which is specifically designed for performance and security. Newtonsoft.Json is very flexible by default. Under the default configuration, you will hardly encounter problems with deserialization, although many of these problems are caused by lax json structures or type declarations.
expansion pack
The default basic package does not include NewtonsoftJson functionality, and you need to additionally refer to the WebApiClientCore.Extensions.NewtonsoftJson extension package.
配置[可选]#
// ConfigureNewtonsoftJson
services.AddHttpApi<IUserApi>().ConfigureNewtonsoftJson(o =>
{
o.JsonSerializeOptions.NullValueHandling = NullValueHandling.Ignore;
});
Declaring characteristics
Use [JsonNetReturn] to replace built-in [JsonReturn], and [JsonNetContent] to replace built-in [JsonContent]
/// <summary>
/// 用户操作接口
/// </summary>
[JsonNetReturn]
public interface IUserApi
{
[HttpPost("/users")]
Task PostAsync([JsonNetContent] User user);
}
JsonRpc call
In very few scenarios, developers may encounter interfaces called by JsonRpc. Since the protocol is not very popular, WebApiClientCore provides support for this feature as the WebApiClientCore.Extensions.JsonRpc extension package. Use [JsonRpcMethod] to modify the Rpc method and use [JsonRpcParam] to modify the Rpc parameters That's enough.
JsonRpc statement
[HttpHost("http://localhost:5000/jsonrpc")]
public interface IUserApi
{
[JsonRpcMethod("add")]
ITask<JsonRpcResult<User>> AddAsync([JsonRpcParam] string name, [JsonRpcParam] int age, CancellationToken token = default);
}
JsonRpc Data Package
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}