Preface
I am WebApiClient The author of the library, who is currently developing his version of.netcore, after sorting out his readme, think about how this might be useful for everyone, and perhaps give WebApiClient More people, so I'll post readme here as a blog.
WebApiClientCore
WebApiClient.JIT The.netcore version of HttpClient, a declarative Http client library that integrates high performance and scalability, is particularly suitable for restful resource requests for micro services and for various non-standard HTTP interface requests.
PackageReference
<PackageReference Include="WebApiClientCore" Version="1.0.0-beta1" />
Project Reasons
- WebApiClient is a great tool that unifies api across different frameworks and platforms
- WebApiClient is not good enough, it can be better under.netcore, but it has to be compatible with.net45 at the expense of starting all frameworks
Relative Change
- Use System.Text.Json replace Json.net To improve serialization performance
- Remove the HttpApiFactory and HttApiConfig functions, using Microsoft.Extensions.Http HttpClientFactory
- Remove AOT functionality, leaving only Emit-dependent runtime agents
- Efficient ActionInvoker with different treatment for returning Task<>and ITask<>
- All features become middleware, piping each feature and generating an Action execution delegation
- Well-designed HttpContext, ApiRequestContext, ApiParameterContext, and ApiResponseContext
Benchmark
WebApiClientCore,WebApiClient.JIT Compared to native HttpClient performance, WebApiClientCore has little performance degradation.
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 |
Declarative Interface Definition
- Supports Task, Task<>and ITask<>three asynchronous returns
- Supports automatic conversion of models to four request formats: Xml, Json, Form, and FormData
- Supports HttpResponseMessage, byte[], string, and Stream native type return content
- Supports native HttpContent types such as StringContent as request parameters directly
- Built-in rich common attributes (ActionAttribute and ParameterAttribute) that meet a variety of environments
- Built-in common parameter types such as FormDataFile, while supporting custom IApiParameter parameter types as parameter values
- Supports user-defined IApiActionAttribute, IApiParameterAttribue, IApiReturnAttribute, and IApiFilterAttribute
The code declared below is to use WebApiClientCore.Extensions.OpenApi Tool generates openApi documents in reverse
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); } }
Another example is WebApiClientCore.Extensions.OAuths.IOAuthClient interface declaration
using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore.Attributes; namespace WebApiClientCore.Extensions.OAuths { /// <summary> ///Define Token Client Interface /// </summary> [LoggingFilter] [XmlReturn(Enable = false)] [JsonReturn(EnsureMatchAcceptContentType = false, EnsureSuccessStatusCode = false)] public interface IOAuthClient { /// <summary> ///as client_credentials authorization method to acquire token /// </summary> /// <param name="endpoint">token request address</param> /// <param name="credentials">identity information</param> /// <returns></returns> [HttpPost] [FormField("grant_type", "client_credentials")] Task<TokenResult> RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] ClientCredentials credentials); /// <summary> ///Get token by password authorization /// </summary> /// <param name="endpoint">token request address</param> /// <param name="credentials">identity information</param> /// <returns></returns> [HttpPost] [FormField("grant_type", "password")] Task<TokenResult> RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] PasswordCredentials credentials); /// <summary> ///Refresh token /// </summary> /// <param name="endpoint">token request address</param> /// <param name="credentials">identity information</param> /// <returns></returns> [HttpPost] [FormField("grant_type", "refresh_token")] Task<TokenResult> RefreshTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] RefreshTokenCredentials credentials); } }
Service Registration and Acquisition
Service Registration
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/"); });
Service Acquisition
public class MyService { private readonly IpetApi petApi; // Constructor Injection IpetApi public MyService(IpetApi petApi) { tihs.petApi = petApi; } }
Request and Response Logs
Declaring [LoggingFilter] on the entire Interface or a Method outputs the contents of the request and response to the LoggingFactory.
If you want to exclude a Method that does not print logs (such as a high-traffic transport interface), you can exclude this Method by declaring [LoggingFilter(Enable = false)] in the Method.
Accpet ContentType
This controls what content format the client expects the server to return, such as JSON or xml. The default configuration value is Accept: application/json; q=0.01, application/xml; q=0.01
If you want JSON to take precedence, you can declare [JsonReturn] on an Interface or Method and the request becomes Accept: application/json, application/xml; q=0.01
If you want to disable one of these, such as xml, you can declare [XmlReturn(Enable = false)] on an Interface or Method, and the request becomes Accept: application/json; q=0.01
Request Conditions Retry
With an ITask<>asynchronous declaration, there is an extension of Retry, which can meet certain criteria for capturing an Exception or response model.
var result = await youApi.GetModelAsync(id: "id001") .Retry(maxCount: 3) .WhenCatch<Exception>() .WhenResult(r => r.ErrorCode > 0);
OAuths&Token
Use WebApiClientCore.Extensions.OAuths Extend to easily support token acquisition, refresh and Application
1 Register the corresponding type of TokenProvider
// Register and configure token provider options for interfaces services.AddClientCredentialsTokenProvider<IpetApi>(o => { o.Endpoint = new Uri("http://localhost:6000/api/tokens"); o.Credentials.Client_id = "clientId"; o.Credentials.Client_secret = "xxyyzz"; });
2 Declare the corresponding Token attribute
/// <summary> ///User Operational Interface /// </summary> [ClientCredentialsToken] public interface IpetApi { ... }
3 Other operations
Empty Token and force refresh for non-expired tokens
var providers = serviceProvider.GetServices<ITokenProvider>(); foreach(var item in providers) { // Force token clearance to support next acquisition of a new token item.ClearToken(); }
Customize Token app, get token value, how to use your own has the final say
class MyTokenAttribute : ClientCredentialsTokenAttribute { protected override void UseTokenResult(ApiRequestContext context, TokenResult tokenResult) { context.HttpContext.RequestMessage.Headers.TryAddWithoutValidation("xxx-header", tokenResult.Access_token); } } /// <summary> ///User Operational Interface /// </summary> [MyToken] public interface IpetApi { ... }