diff --git a/App/.config/dotnet-tools.json b/App/.config/dotnet-tools.json new file mode 100644 index 00000000..4f257cf0 --- /dev/null +++ b/App/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.6", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/App/ApiClients/IUserApi.cs b/App/ApiClients/IUserApi.cs new file mode 100644 index 00000000..46b2ec6d --- /dev/null +++ b/App/ApiClients/IUserApi.cs @@ -0,0 +1,61 @@ +using App.Models; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using WebApiClientCore; +using WebApiClientCore.Attributes; +using WebApiClientCore.Parameters; + +namespace App.Clients +{ + /// + /// 用户操作接口 + /// + [OAuthToken] + [LoggingFilter] + public interface IUserApi : IHttpApi + { + [HttpGet("api/users/{account}")] + ITask GetAsync(string account); + + [HttpGet("api/users/{account}")] + ITask GetAsStringAsync(string account, CancellationToken token = default); + + [HttpGet("api/users/{account}")] + ITask GetAsByteArrayAsync(string account, CancellationToken token = default); + + [HttpGet("api/users/{account}")] + ITask GetAsStreamAsync(string account, CancellationToken token = default); + + [HttpGet("api/users/{account}")] + ITask GetAsModelAsync(string account, CancellationToken token = default); + + [JsonReturn] + [HttpGet("api/users/{account}")] + ITask GetExpectJsonAsync(string account, CancellationToken token = default); + + [XmlReturn] + [HttpGet("api/users/{account}")] + ITask GetExpectXmlAsync(string account, CancellationToken token = default); + + + + [HttpPost("api/users/body")] + Task PostByJsonAsync([JsonContent] User user, CancellationToken token = default); + + [HttpPost("api/users/body")] + Task PostByXmlAsync([XmlContent] User user, CancellationToken token = default); + + [HttpPost("api/users/form")] + Task PostByFormAsync([FormContent] User user, CancellationToken token = default); + + [HttpPost("api/users/formdata")] + Task PostByFormDataAsync([FormDataContent] User user, FormDataFile file, CancellationToken token = default); + + + + [HttpDelete("api/users/{account}")] + Task DeleteAsync(string account); + } +} diff --git a/App/Clients/UserHostedService.cs b/App/ApiClients/UserHostedService.cs similarity index 52% rename from App/Clients/UserHostedService.cs rename to App/ApiClients/UserHostedService.cs index dca6111d..5ca775c1 100644 --- a/App/Clients/UserHostedService.cs +++ b/App/ApiClients/UserHostedService.cs @@ -8,13 +8,13 @@ namespace App.Clients { public class UserHostedService : BackgroundService - { - private readonly IServiceProvider service; - private readonly ILogger logger; + { + private readonly IServiceScopeFactory serviceScopeFactory; + private readonly ILogger logger; - public UserHostedService(IServiceProvider service, ILogger logger) - { - this.service = service; + public UserHostedService(IServiceScopeFactory serviceScopeFactory, ILogger logger) + { + this.serviceScopeFactory = serviceScopeFactory; this.logger = logger; } @@ -22,9 +22,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { - using var scope = this.service.CreateScope(); - var userService = scope.ServiceProvider.GetService(); - await userService.RunRequestAsync(); + using var scope = this.serviceScopeFactory.CreateScope(); + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.RunRequestsAsync(); } catch (Exception ex) { diff --git a/App/Clients/UserService.cs b/App/ApiClients/UserService.cs similarity index 84% rename from App/Clients/UserService.cs rename to App/ApiClients/UserService.cs index bb0b297b..0e77c99d 100644 --- a/App/Clients/UserService.cs +++ b/App/ApiClients/UserService.cs @@ -1,4 +1,5 @@ -using System; +using App.Models; +using System; using System.Threading.Tasks; using WebApiClientCore; using WebApiClientCore.Parameters; @@ -14,7 +15,7 @@ public UserService(IUserApi userApi) this.userApi = userApi; } - public async Task RunRequestAsync() + public async Task RunRequestsAsync() { var user = new User { @@ -26,17 +27,15 @@ public async Task RunRequestAsync() }; // 上传的文件 - var file = new FormDataFile("文件TextFile.txt"); + var file = new FormDataFile("TextFile.txt"); var response = await userApi.GetAsync(account: "get1"); - var @string = await userApi.GetAsStringAsync(account: "get2"); - var jsonText = await userApi.GetExpectJsonAsync(account: "json"); - var xmlText = await this.userApi.GetExpectXmlAsync(account: "xml"); - var byteArray = await userApi.GetAsByteArrayAsync(account: "get3"); var stream = await userApi.GetAsStreamAsync(account: "get4"); var model = await userApi.GetAsModelAsync(account: "get5"); + var jsonUser = await userApi.GetExpectJsonAsync(account: "json"); + var xmlUser = await this.userApi.GetExpectXmlAsync(account: "xml"); var post1 = await userApi.PostByJsonAsync(user); var post2 = await userApi.PostByXmlAsync(user); diff --git a/App/App.csproj b/App/App.csproj index 207f2462..91585e3f 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -2,7 +2,8 @@ Exe - net6.0 + enable + netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 false false @@ -11,7 +12,6 @@ - diff --git a/App/Attributes/ServiceNameAttribute.cs b/App/Attributes/ServiceNameAttribute.cs deleted file mode 100644 index a8a51d12..00000000 --- a/App/Attributes/ServiceNameAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.Options; -using System.Threading.Tasks; -using System.Threading; -using System; -using WebApiClientCore.Attributes; -using WebApiClientCore; -using App.Services; -using Microsoft.Extensions.DependencyInjection; - -namespace App.Attributes -{ - /// - /// 表示对应的服务名 - /// - public class ServiceNameAttribute : ApiActionAttribute - - { - public ServiceNameAttribute(string name) - { - Name = name; - OrderIndex = int.MinValue; - } - - public string Name { get; set; } - - public override async Task OnRequestAsync(ApiRequestContext context) - { - await Task.CompletedTask; - IServiceProvider sp = context.HttpContext.ServiceProvider; - HostProvider hostProvider = sp.GetRequiredService(); - //服务名也可以在接口配置时挂在Properties中 - string host = hostProvider.ResolveService(this.Name); - HttpApiRequestMessage requestMessage = context.HttpContext.RequestMessage; - //和原有的Uri组合并覆盖原有Uri - //并非一定要这样实现,只要覆盖了RequestUri,即完成了替换 - requestMessage.RequestUri = requestMessage.MakeRequestUri(new Uri(host)); - } - - - } -} diff --git a/App/Clients/DynamicHostDemoHostedService.cs b/App/Clients/DynamicHostDemoHostedService.cs deleted file mode 100644 index 0e0200e8..00000000 --- a/App/Clients/DynamicHostDemoHostedService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace App.Clients -{ - public class DynamicHostDemoHostedService : BackgroundService - { - private readonly IServiceProvider service; - private readonly ILogger logger; - - public DynamicHostDemoHostedService(IServiceProvider service, ILogger logger) - { - this.service = service; - this.logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - using var scope = this.service.CreateScope(); - var dynamicHostDemoService = scope.ServiceProvider.GetService(); - await dynamicHostDemoService.RunRequestAsync(); - } - catch (Exception ex) - { - this.logger.LogError(ex, ex.Message); - } - } - } -} diff --git a/App/Clients/DynamicHostDemoService.cs b/App/Clients/DynamicHostDemoService.cs deleted file mode 100644 index 033c7c9e..00000000 --- a/App/Clients/DynamicHostDemoService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace App.Clients -{ - public class DynamicHostDemoService - { - private readonly IDynamicHostDemo dynamicHostDemo; - - public DynamicHostDemoService(IDynamicHostDemo dynamicHostDemo) - { - this.dynamicHostDemo = dynamicHostDemo; - } - - internal async Task RunRequestAsync() - { - var r1 = await dynamicHostDemo.ByUrlString("http://www.soso.com"); - var r2 = await dynamicHostDemo.ByAttribute(); - var r3 = await dynamicHostDemo.ByFilter(); - } - } -} diff --git a/App/Clients/IDynamicHostDemo.cs b/App/Clients/IDynamicHostDemo.cs deleted file mode 100644 index 3518abaf..00000000 --- a/App/Clients/IDynamicHostDemo.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net.Http; -using App.Attributes; -using App.Filters; -using WebApiClientCore; -using WebApiClientCore.Attributes; - -namespace App.Clients -{ - [LoggingFilter] - public interface IDynamicHostDemo - { - [HttpGet] - ITask ByUrlString([Uri] string urlString); - - [UriFilter] - [HttpGet] - ITask ByFilter(); - - - [HttpGet] - [ServiceName("baiduService")] - ITask ByAttribute(); - } -} diff --git a/App/Clients/IUserApi.cs b/App/Clients/IUserApi.cs deleted file mode 100644 index 972e516f..00000000 --- a/App/Clients/IUserApi.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using WebApiClientCore; -using WebApiClientCore.Attributes; -using WebApiClientCore.Parameters; - -namespace App.Clients -{ - /// - /// 用户操作接口 - /// - [LoggingFilter] - [OAuthToken] - public interface IUserApi : IHttpApi - { - [HttpGet("api/users/{account}")] - ITask GetAsync([Required] string account); - - [HttpGet("api/users/{account}")] - ITask GetAsStringAsync([Required] string account, CancellationToken token = default); - - - [HttpGet("api/users/{account}")] - [JsonReturn] - ITask GetExpectJsonAsync([Required] string account, CancellationToken token = default); - - - [HttpGet("api/users/{account}")] - [XmlReturn] - ITask GetExpectXmlAsync([Required] string account, CancellationToken token = default); - - - - [HttpGet("api/users/{account}")] - ITask GetAsByteArrayAsync([Required] string account, CancellationToken token = default); - - [HttpGet("api/users/{account}")] - ITask GetAsStreamAsync([Required] string account, CancellationToken token = default); - - [HttpGet("api/users/{account}")] - ITask GetAsModelAsync([Required] string account, CancellationToken token = default); - - - [HttpPost("api/users/body")] - Task PostByJsonAsync([Required, JsonContent] User user, CancellationToken token = default); - - [HttpPost("api/users/body")] - Task PostByXmlAsync([Required, XmlContent] User user, CancellationToken token = default); - - - - [HttpPost("api/users/form")] - Task PostByFormAsync([Required, FormContent] User user, CancellationToken token = default); - - [HttpPost("api/users/formdata")] - Task PostByFormDataAsync([Required, FormDataContent] User user, FormDataFile file, CancellationToken token = default); - - - - [HttpDelete("api/users/{account}")] - Task DeleteAsync([Required] string account); - } -} diff --git a/App/Clients/IUserApi_ParameterStyle.cs b/App/Clients/IUserApi_ParameterStyle.cs deleted file mode 100644 index 463147cd..00000000 --- a/App/Clients/IUserApi_ParameterStyle.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using WebApiClientCore; -using WebApiClientCore.Attributes; -using WebApiClientCore.Parameters; - -namespace App.Clients -{ - /// - /// 用户操作接口 - /// - [LoggingFilter] - [HttpHost("http://localhost:6000/")] - public interface IUserApi_ParameterStyle : IHttpApi - { - [HttpGet("api/users/{account}")] - Task GetAsync([Required, Parameter(Kind.Path)]string account); - - [HttpGet("api/users/{account}")] - Task GetAsStringAsync([Required, Parameter(Kind.Path)]string account, CancellationToken token = default); - - [HttpGet("api/users/{account}")] - Task GetAsByteArrayAsync([Required, Parameter(Kind.Path)]string account, CancellationToken token = default); - - [HttpGet("api/users/{account}")] - Task GetAsStreamAsync([Required, Parameter(Kind.Path)]string account, CancellationToken token = default); - - [HttpGet("api/users/{account}")] - Task GetAsModelAsync([Required, Parameter(Kind.Path)]string account, CancellationToken token = default); - - - - - [HttpPost("api/users/body")] - Task PostByJsonAsync([Required, Parameter(Kind.JsonBody)]User user, CancellationToken token = default); - - [HttpPost("api/users/body")] - Task PostByXmlAsync([Required, Parameter(Kind.XmlBody)]User user, CancellationToken token = default); - - - [HttpPost("api/users/form")] - Task PostByFormAsync([Required, Parameter(Kind.Form)]User user, CancellationToken token = default); - - [HttpPost("api/users/formdata")] - Task PostByFormDataAsync([Required, Parameter(Kind.FormData)]User user, FormDataFile file, CancellationToken token = default); - - - [HttpDelete("api/users/{account}")] - Task DeleteAsync([Required] string account); - } -} diff --git a/App/Controllers/TokenFilterAttribute.cs b/App/Controllers/TokenFilterAttribute.cs index 1884bf54..f5101a94 100644 --- a/App/Controllers/TokenFilterAttribute.cs +++ b/App/Controllers/TokenFilterAttribute.cs @@ -1,8 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace App.Controllers diff --git a/App/Controllers/UsersController.cs b/App/Controllers/UsersController.cs index 365304f5..fc7d7c40 100644 --- a/App/Controllers/UsersController.cs +++ b/App/Controllers/UsersController.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Http; +using App.Models; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace App.Controllers { + [TokenFilter] [ApiController] [Route("api/[controller]")] - [TokenFilter] public class UsersController : ControllerBase { [HttpGet("{account}")] @@ -28,9 +29,9 @@ public User PostByForm([FromForm] User formUser) } [HttpPost("formdata")] - public User PostByFormData([FromForm] User formDatauser, IFormFile file) + public User PostByFormData([FromForm] User formDataUser, IFormFile file) { - return formDatauser; + return formDataUser; } [HttpDelete("{account}")] diff --git a/App/Extensions/DIExtensions4DynamicHost.cs b/App/Extensions/DIExtensions4DynamicHost.cs deleted file mode 100644 index 254f0a9b..00000000 --- a/App/Extensions/DIExtensions4DynamicHost.cs +++ /dev/null @@ -1,26 +0,0 @@ -using App.Clients; -using App.Services; -using Microsoft.Extensions.DependencyInjection; - -namespace App.Extensions -{ - internal static class DIExtensions4DynamicHost - { - /// - /// 动态Host的Demo相关服务注册 - /// - /// - /// - public static IServiceCollection AddDynamicHostSupport(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddHttpApi().ConfigureHttpApi(options => { - options.Properties.Add("serviceName", "microsoftService"); - }); - services.AddScoped(); - services.AddHostedService(); - return services; - } - } -} diff --git a/App/Filters/UriFilterAttribute.cs b/App/Filters/UriFilterAttribute.cs deleted file mode 100644 index 90a9566f..00000000 --- a/App/Filters/UriFilterAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Threading.Tasks; -using App.Services; -using Microsoft.Extensions.DependencyInjection; -using WebApiClientCore; -using WebApiClientCore.Attributes; - -namespace App.Filters -{ - /// - ///用来处理动态Uri的拦截器 - /// - public class UriFilterAttribute : ApiFilterAttribute - { - public override Task OnRequestAsync(ApiRequestContext context) - { - var options = context.HttpContext.HttpApiOptions; - //获取注册时为服务配置的服务名 - options.Properties.TryGetValue("serviceName", out object serviceNameObj); - string serviceName = serviceNameObj as string; - IServiceProvider sp = context.HttpContext.ServiceProvider; - HostProvider hostProvider = sp.GetRequiredService(); - string host = hostProvider.ResolveService(serviceName); - HttpApiRequestMessage requestMessage = context.HttpContext.RequestMessage; - //和原有的Uri组合并覆盖原有Uri - //并非一定要这样实现,只要覆盖了RequestUri,即完成了替换 - requestMessage.RequestUri = requestMessage.MakeRequestUri(new Uri(host)); - return Task.CompletedTask; - } - - public override Task OnResponseAsync(ApiResponseContext context) - { - //不处理响应的信息 - return Task.CompletedTask; - } - } -} diff --git a/App/Models/Gender.cs b/App/Models/Gender.cs new file mode 100644 index 00000000..f4ae9ff6 --- /dev/null +++ b/App/Models/Gender.cs @@ -0,0 +1,11 @@ +namespace App.Models +{ + /// + /// 性别 + /// + public enum Gender + { + Female = 0, + Male = 1 + } +} diff --git a/App/User.cs b/App/Models/User.cs similarity index 63% rename from App/User.cs rename to App/Models/User.cs index 11200658..1a475c46 100644 --- a/App/User.cs +++ b/App/Models/User.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; using WebApiClientCore.Serialization.JsonConverters; -namespace App +namespace App.Models { /// /// 表示用户模型 @@ -12,13 +12,13 @@ public class User { [Required] [StringLength(10, MinimumLength = 1)] - public string Account { get; set; } + public string Account { get; set; } = string.Empty; [Required] [StringLength(10, MinimumLength = 1)] - public string Password { get; set; } + public string Password { get; set; } = string.Empty; - public string NickName { get; set; } + public string? NickName { get; set; } [JsonDateTime("yyyy年MM月dd日")] public DateTime? BirthDay { get; set; } @@ -26,15 +26,6 @@ public class User public Gender Gender { get; set; } [JsonIgnore] - public string Email { get; set; } - } - - /// - /// 性别 - /// - public enum Gender - { - Female = 0, - Male = 1 + public string? Email { get; set; } } } diff --git a/App/Program.cs b/App/Program.cs index ffd6de57..0c0f82a7 100644 --- a/App/Program.cs +++ b/App/Program.cs @@ -25,7 +25,7 @@ public static void Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) { return Host - .CreateDefaultBuilder(args) + .CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/App/README.md b/App/README.md index 0bbbf3c9..92ef50e6 100644 --- a/App/README.md +++ b/App/README.md @@ -2,10 +2,9 @@ 这是应用例子,为了方便,服务端和客户端都在同一个程序进程内 ### 服务端 -Controllers为服务端,TokensController模拟token发放的服务端,UsersController模拟用户资源服务器 +Controllers 为服务端,TokensController 模拟 token 发放的服务端,UsersController 模拟用户资源服务器 ### 客户端 -* Clients.IUserApi为WebApiClientCore的声明式接口 -* Clients.IUserApi.ParameterStyle为Parameter式声明,两种效果相同 -* Clients.UserService为包装的服务,注入了IUserApi接口 -* Clients.UserHostedService为后台服务,启动时获取UserService实例并运行 +* ApiClients.IUserApi为WebApiClientCore的声明式接口 +* ApiClients.UserService为包装的服务,注入了IUserApi接口 +* ApiClients.UserHostedService为后台服务,启动时获取UserService实例并运行 diff --git a/App/Services/HostProvider.cs b/App/Services/HostProvider.cs deleted file mode 100644 index 8c9c6fb1..00000000 --- a/App/Services/HostProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace App.Services -{ - public interface IDBProvider - { - string SelectServiceUri(string serviceName); - } - public class DBProvider : IDBProvider - { - public string SelectServiceUri(string serviceName) - { - if (serviceName == "baiduService") return "https://www.baidu.com"; - if (serviceName == "microsoftService") return "https://www.microsoft.com"; - return string.Empty; - } - } - - public class HostProvider - { - private readonly IDBProvider dbProvider; - - public HostProvider(IDBProvider dbProvider) - { - this.dbProvider = dbProvider; - //将HostProvider放到依赖注入容器中,即可从容器获取其它服务来实现动态的服务地址查询 - } - - internal string ResolveService(string name) - { - //如何获取动态的服务地址由你自己决定,此处仅以简单的接口定义简要说明 - return dbProvider.SelectServiceUri(name); - } - } -} diff --git a/App/Startup.cs b/App/Startup.cs index ee81385b..0d0a8787 100644 --- a/App/Startup.cs +++ b/App/Startup.cs @@ -1,5 +1,4 @@ using App.Clients; -using App.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -48,13 +47,11 @@ public void ConfigureServices(IServiceCollection services) // ȫĬ services .AddWebApiClient() - .ConfigureHttpApi(o => { }) .UseJsonFirstApiActionDescriptor(); // עuserApi services.AddHttpApi(typeof(IUserApi)).ConfigureHttpApi(o => { - o.UseLogging = Environment.IsDevelopment(); o.HttpHost = new Uri("http://localhost:5000/"); }); @@ -68,7 +65,6 @@ public void ConfigureServices(IServiceCollection services) // userApiͻ˺̨ services.AddScoped().AddHostedService(); - services.AddDynamicHostSupport(); } /// diff --git "a/App/\346\226\207\344\273\266TextFile.txt" "b/App/\346\226\207\344\273\266TextFile.txt" deleted file mode 100644 index b2a64c1c..00000000 --- "a/App/\346\226\207\344\273\266TextFile.txt" +++ /dev/null @@ -1 +0,0 @@ -这是上传的文件内容 \ No newline at end of file diff --git a/AppAot/AppHostedService.cs b/AppAot/AppHostedService.cs index f2229e2a..02f449b2 100644 --- a/AppAot/AppHostedService.cs +++ b/AppAot/AppHostedService.cs @@ -7,24 +7,23 @@ namespace AppAot { class AppHostedService : BackgroundService - { + { private readonly IServiceScopeFactory serviceScopeFactory; private readonly ILogger logger; - public AppHostedService( + public AppHostedService( IServiceScopeFactory serviceScopeFactory, ILogger logger) - { + { this.serviceScopeFactory = serviceScopeFactory; this.logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { + { using var scope = this.serviceScopeFactory.CreateScope(); var api = scope.ServiceProvider.GetRequiredService(); var appData = await api.GetAppDataAsync(); - appData = await api.GetAppData2Async(); this.logger.LogInformation($"WebpackCompilationHash: {appData.WebpackCompilationHash}"); } } diff --git a/AppAot/ICloudflareApi.cs b/AppAot/ICloudflareApi.cs index 6bdd6b7e..3e24eb2d 100644 --- a/AppAot/ICloudflareApi.cs +++ b/AppAot/ICloudflareApi.cs @@ -1,17 +1,13 @@ using System.Threading.Tasks; -using WebApiClientCore; using WebApiClientCore.Attributes; namespace AppAot { - [HttpHost("https://www.cloudflare-cn.com")] [LoggingFilter] + [HttpHost("https://www.cloudflare-cn.com")] public interface ICloudflareApi { [HttpGet("/page-data/app-data.json")] - Task GetAppDataAsync(); - - [HttpGet("/page-data/app-data.json")] - ITask GetAppData2Async(); + Task GetAppDataAsync(); } } diff --git a/AppAot/Program.cs b/AppAot/Program.cs index 50ca5dd8..924666cf 100644 --- a/AppAot/Program.cs +++ b/AppAot/Program.cs @@ -14,10 +14,7 @@ static void Main(string[] args) .AddWebApiClient() .ConfigureHttpApi(options => // json SG生成器配置 { - var jsonContext = AppJsonSerializerContext.Default; - options.JsonSerializeOptions.TypeInfoResolverChain.Insert(0, jsonContext); - options.JsonDeserializeOptions.TypeInfoResolverChain.Insert(0, jsonContext); - options.KeyValueSerializeOptions.GetJsonSerializerOptions().TypeInfoResolverChain.Insert(0, jsonContext); + options.PrependJsonSerializerContext(AppJsonSerializerContext.Default); }); services.AddHttpApi(); diff --git a/Directory.Build.props b/Directory.Build.props index 16d7cf4b..4b473b3a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,8 @@ - 2.0.8 + 2.1.5 Copyright © laojiu 2017-2024 - IDE0057;IDE0290 + IDE0290;NETSDK1138 diff --git a/README.md b/README.md index b81af1a1..f03a11d4 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,32 @@ +[README](README.md) | [中文文档](README_zh.md) + ## WebApiClient                  -集高性能高可扩展性于一体的声明式http客户端库。 +A REST API library with better functionality, performance, and scalability than refit. -### 功能特性 -#### 语义化声明 -客户端的开发,只需语义化的声明接口。 +### Features +#### Semantic Declaration +Client development only requires semantic declaration of C# interfaces. -#### 多样序列化 -支持json、xml、form等序列化和其它自定义序列化方式。 +#### Diverse serialization +Supports json, xml, form and other custom serialization methods. -#### 裁剪与AOT -支持.net8的代码完全裁剪和AOT发布。 +#### Full trimmed and AOT +Supports full trimmed and AOT publishing of .NET8. -#### 面向切面 -支持多种拦截器、过滤器、日志、重试、缓存自定义等功能。 +#### Aspect-Oriented Programming +Supports multiple interceptors, filters, logs, retries, custom caches and other aspects. -#### 语法分析 -提供接口声明的语法分析与提示,帮助开发者声明接口时避免使用不当的语法。 +#### Code Syntax Analysis +Provides syntax analysis and prompts for interface code declarations to help developers avoid using improper syntax when declaring interfaces. -#### 快速接入 -支持OAuth2与token管理扩展包,方便实现身份认证和授权。 +#### Quick access +Supports OAuth2 and token management extension packages to facilitate identity authentication and authorization. -#### 自动代码 -支持将本地或远程OpenApi文档解析生成WebApiClientCore接口代码的dotnet tool,简化接口声明的工作量 +#### Swagger to code +Supports parsing local or remote OpenApi documents to generate WebApiClientCore interface code, which simplifies the workload of interface declaration. -#### 性能强劲 -在[BenchmarkDotNet](WebApiClientCore.Benchmarks/results)中,各种请求下2.X倍性能领先于同类产品[refit](https://github.com/reactiveui/refit)。 +#### Powerful performance +In [BenchmarkDotNet](WebApiClientCore.Benchmarks/results), the performance is 2.X times ahead of the similar product [refit](https://github.com/reactiveui/refit) under various requests. -### 文档支持 -https://webapiclient.github.io/ +### Documentation support +[https://webapiclient.github.io/](https://webapiclient.github.io/en/) diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 00000000..ee68c603 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,32 @@ +[README](README.md) | [中文文档](README_zh.md) + +## WebApiClient                  +一个在功能、性能和可扩展性均优于 refit 的 REST API 库 + +### 功能特性 +#### 语义化声明 +客户端的开发,只需语义化的声明接口。 + +#### 多样序列化 +支持json、xml、form等序列化和其它自定义序列化方式。 + +#### 裁剪与AOT +支持.net8的代码完全裁剪和AOT发布。 + +#### 面向切面 +支持多种拦截器、过滤器、日志、重试、缓存自定义等功能。 + +#### 语法分析 +提供接口声明的语法分析与提示,帮助开发者声明接口时避免使用不当的语法。 + +#### 快速接入 +支持OAuth2与token管理扩展包,方便实现身份认证和授权。 + +#### 自动代码 +支持将本地或远程OpenApi文档解析生成WebApiClientCore接口代码的dotnet tool,简化接口声明的工作量 + +#### 性能强劲 +在[BenchmarkDotNet](WebApiClientCore.Benchmarks/results)中,各种请求下2.X倍性能领先于同类产品[refit](https://github.com/reactiveui/refit)。 + +### 文档支持 +https://webapiclient.github.io/ diff --git a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs index ea08740e..fafee9f0 100644 --- a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs +++ b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace WebApiClientCore @@ -13,7 +14,8 @@ public abstract class ApiActionDescriptor /// /// 获取所在接口类型 /// 这个值不一定是声明方法的接口类型 - /// + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public abstract Type InterfaceType { get; protected set; } /// @@ -55,6 +57,6 @@ public abstract class ApiActionDescriptor /// /// 获取自定义数据存储的字典 /// - public abstract ConcurrentDictionary Properties { get; protected set; } + public abstract ConcurrentDictionary Properties { get; protected set; } } } diff --git a/WebApiClientCore.Abstractions/ApiDataTypeDescriptor.cs b/WebApiClientCore.Abstractions/ApiDataTypeDescriptor.cs index 6a08b6e0..a6ea5660 100644 --- a/WebApiClientCore.Abstractions/ApiDataTypeDescriptor.cs +++ b/WebApiClientCore.Abstractions/ApiDataTypeDescriptor.cs @@ -23,7 +23,7 @@ public abstract class ApiDataTypeDescriptor public abstract bool IsRawStream { get; protected set; } /// - /// 获取是否为原始类型的byte[] + /// 获取是否为原始类型的 byte[] /// public abstract bool IsRawByteArray { get; protected set; } diff --git a/WebApiClientCore.Abstractions/CodeAnalysis/DynamicDependencyAttribute.cs b/WebApiClientCore.Abstractions/CodeAnalysis/DynamicDependencyAttribute.cs new file mode 100644 index 00000000..82b6f844 --- /dev/null +++ b/WebApiClientCore.Abstractions/CodeAnalysis/DynamicDependencyAttribute.cs @@ -0,0 +1,32 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// 表示动态依赖属性 + /// + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = true, Inherited = false)] + sealed class DynamicDependencyAttribute : Attribute + { + /// + /// 获取或设置动态访问的成员类型 + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// 获取或设置依赖的类型 + /// + public Type? Type { get; } + + /// + /// 初始化 类的新实例 + /// + /// 动态访问的成员类型 + /// 依赖的类型 + public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type) + { + this.MemberTypes = memberTypes; + this.Type = type; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 00000000..54bcd902 --- /dev/null +++ b/WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,85 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies the types of dynamically accessed members. + /// + enum DynamicallyAccessedMemberTypes + { + /// + /// All member types are dynamically accessed. + /// + All = -1, + + /// + /// No member types are dynamically accessed. + /// + None = 0, + + /// + /// Public parameterless constructors are dynamically accessed. + /// + PublicParameterlessConstructor = 1, + + /// + /// Public constructors are dynamically accessed. + /// + PublicConstructors = 3, + + /// + /// Non-public constructors are dynamically accessed. + /// + NonPublicConstructors = 4, + + /// + /// Public methods are dynamically accessed. + /// + PublicMethods = 8, + + /// + /// Non-public methods are dynamically accessed. + /// + NonPublicMethods = 16, + + /// + /// Public fields are dynamically accessed. + /// + PublicFields = 32, + + /// + /// Non-public fields are dynamically accessed. + /// + NonPublicFields = 64, + + /// + /// Public nested types are dynamically accessed. + /// + PublicNestedTypes = 128, + + /// + /// Non-public nested types are dynamically accessed. + /// + NonPublicNestedTypes = 256, + + /// + /// Public properties are dynamically accessed. + /// + PublicProperties = 512, + + /// + /// Non-public properties are dynamically accessed. + /// + NonPublicProperties = 1024, + + /// + /// Public events are dynamically accessed. + /// + PublicEvents = 2048, + + /// + /// Non-public events are dynamically accessed. + /// + NonPublicEvents = 4096, + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs new file mode 100644 index 00000000..30a744b9 --- /dev/null +++ b/WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the members accessed dynamically at runtime are considered used. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false)] + sealed class DynamicallyAccessedMembersAttribute : Attribute + { + /// + /// Gets the types of dynamically accessed members. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// Initializes a new instance of the class with the specified member types. + /// + /// The types of dynamically accessed members. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + this.MemberTypes = memberTypes; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/CodeAnalysis/RequiresDynamicCodeAttribute.cs b/WebApiClientCore.Abstractions/CodeAnalysis/RequiresDynamicCodeAttribute.cs new file mode 100644 index 00000000..e94faa6d --- /dev/null +++ b/WebApiClientCore.Abstractions/CodeAnalysis/RequiresDynamicCodeAttribute.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_1 || NET5_0 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the attributed class, constructor, or method requires dynamic code. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + sealed class RequiresDynamicCodeAttribute : Attribute + { + /// + /// Gets the message associated with the requirement for dynamic code. + /// + public string Message { get; } + + /// + /// Initializes a new instance of the class with the specified message. + /// + /// The message associated with the requirement for dynamic code. + public RequiresDynamicCodeAttribute(string message) + { + this.Message = message; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs b/WebApiClientCore.Abstractions/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 00000000..ee93f9d3 --- /dev/null +++ b/WebApiClientCore.Abstractions/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,22 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// 获取或设置对于未引用代码的要求的消息。 + /// + public string Message { get; } + + /// + /// 初始化 类的新实例。 + /// + /// 对于未引用代码的要求的消息。 + public RequiresUnreferencedCodeAttribute(string message) + { + this.Message = message; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/WebApiClientCore.Abstractions/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs new file mode 100644 index 00000000..caa21e39 --- /dev/null +++ b/WebApiClientCore.Abstractions/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs @@ -0,0 +1,52 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// 表示一个用于取消对代码分析器规则的警告的特性 + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + sealed class UnconditionalSuppressMessageAttribute : Attribute + { + /// + /// 获取或设置警告的类别 + /// + public string Category { get; } + + /// + /// 获取或设置要取消的检查标识符 + /// + public string CheckId { get; } + + /// + /// 获取或设置取消警告的理由 + /// + public string? Justification { get; set; } + + /// + /// 获取或设置消息的标识符 + /// + public string? MessageId { get; set; } + + /// + /// 获取或设置取消警告的范围 + /// + public string? Scope { get; set; } + + /// + /// 获取或设置取消警告的目标 + /// + public string? Target { get; set; } + + /// + /// 初始化 类的新实例 + /// + /// 警告的类别 + /// 要取消的检查标识符 + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + this.Category = category; + this.CheckId = checkId; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs b/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs index c3191689..e5b635de 100644 --- a/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs +++ b/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Diagnostics; namespace WebApiClientCore @@ -9,7 +10,7 @@ namespace WebApiClientCore public class ApiRequestContext { /// - /// 获取http上下文 + /// 获取 http 上下文 /// public HttpContext HttpContext { get; } @@ -21,8 +22,9 @@ public class ApiRequestContext /// /// 获取关联的ApiAction描述 /// + [Obsolete("Use ActionDescriptor instead")] [EditorBrowsable(EditorBrowsableState.Never)] - [DebuggerBrowsable(DebuggerBrowsableState.Never)] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] public ApiActionDescriptor ApiAction => this.ActionDescriptor; /// diff --git a/WebApiClientCore.Abstractions/Contexts/ApiResponseContext.cs b/WebApiClientCore.Abstractions/Contexts/ApiResponseContext.cs index 87a50f16..7df11cd3 100644 --- a/WebApiClientCore.Abstractions/Contexts/ApiResponseContext.cs +++ b/WebApiClientCore.Abstractions/Contexts/ApiResponseContext.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace WebApiClientCore { @@ -52,13 +53,29 @@ public Exception? Exception } } + /// + /// 当此请求所依据的连接被中止且因此请求操作应被取消时发出通知 + /// + public CancellationToken RequestAborted { get; } + /// /// Api响应的上下文 /// /// 请求上下文 public ApiResponseContext(ApiRequestContext context) + : this(context, default) + { + } + + /// + /// Api响应的上下文 + /// + /// 请求上下文 + /// 请求取消令牌 + public ApiResponseContext(ApiRequestContext context, CancellationToken requestAborted) : base(context.HttpContext, context.ActionDescriptor, context.Arguments, context.Properties) { + this.RequestAborted = requestAborted; } } } diff --git a/WebApiClientCore.Abstractions/Contexts/HttpContext.cs b/WebApiClientCore.Abstractions/Contexts/HttpContext.cs index 009c9fe8..0789ddfe 100644 --- a/WebApiClientCore.Abstractions/Contexts/HttpContext.cs +++ b/WebApiClientCore.Abstractions/Contexts/HttpContext.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore { /// - /// 表示http上下文 + /// 表示 http 上下文 /// public class HttpContext : HttpClientContext { diff --git a/WebApiClientCore.Abstractions/HttpApiOptions.cs b/WebApiClientCore.Abstractions/HttpApiOptions.cs index 708035b0..161d84ec 100644 --- a/WebApiClientCore.Abstractions/HttpApiOptions.cs +++ b/WebApiClientCore.Abstractions/HttpApiOptions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json; using System.Xml; @@ -15,7 +17,7 @@ public class HttpApiOptions { /// /// 获取或设置Http服务完整主机域名 - /// 例如http://www.abc.com/或http://www.abc.com/path/ + /// 例如 http://www.abc.com/ 或 http://www.abc.com/path/ /// 设置了HttpHost值,HttpHostAttribute将失效 /// public Uri? HttpHost { get; set; } @@ -43,12 +45,12 @@ public class HttpApiOptions /// - /// 获取json序列化选项 + /// 获取 json 序列化选项 /// public JsonSerializerOptions JsonSerializeOptions { get; } = CreateJsonSerializeOptions(); /// - /// 获取json反序列化选项 + /// 获取 json 反序列化选项 /// public JsonSerializerOptions JsonDeserializeOptions { get; } = CreateJsonDeserializeOptions(); @@ -63,7 +65,7 @@ public class HttpApiOptions public XmlReaderSettings XmlDeserializeOptions { get; } = new XmlReaderSettings(); /// - /// 获取keyValue序列化选项 + /// 获取 keyValue 序列化选项 /// public KeyValueSerializerOptions KeyValueSerializeOptions { get; } = new KeyValueSerializerOptions(); @@ -96,12 +98,29 @@ private static JsonSerializerOptions CreateJsonSerializeOptions() /// 创建反序列化JsonSerializerOptions /// /// + [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "JsonCompatibleConverter.EnumReader使用前已经判断RuntimeFeature.IsDynamicCodeSupported")] private static JsonSerializerOptions CreateJsonDeserializeOptions() { var options = CreateJsonSerializeOptions(); - options.Converters.Add(JsonCompatibleConverter.EnumReader); + if (RuntimeFeature.IsDynamicCodeSupported) + { + options.Converters.Add(JsonCompatibleConverter.EnumReader); + } options.Converters.Add(JsonCompatibleConverter.DateTimeReader); return options; } + +#if NET8_0_OR_GREATER + /// + /// 插入指定的到所有序列化选项的TypeInfoResolverChain的最前位置 + /// + /// + public void PrependJsonSerializerContext(System.Text.Json.Serialization.JsonSerializerContext context) + { + this.JsonSerializeOptions.TypeInfoResolverChain.Insert(0, context); + this.JsonDeserializeOptions.TypeInfoResolverChain.Insert(0, context); + this.KeyValueSerializeOptions.GetJsonSerializerOptions().TypeInfoResolverChain.Insert(0, context); + } +#endif } } \ No newline at end of file diff --git a/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs b/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs index 3de36e79..20e58ce0 100644 --- a/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs +++ b/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs @@ -8,12 +8,12 @@ namespace WebApiClientCore { /// - /// 表示httpApi的请求消息 + /// 表示 httpApi 的请求消息 /// public abstract class HttpApiRequestMessage : HttpRequestMessage { /// - /// 返回使用uri值合成的请求URL + /// 返回使用 uri 值合成的请求URL /// /// uri值 /// @@ -29,20 +29,20 @@ public abstract class HttpApiRequestMessage : HttpRequestMessage /// /// 添加字段到已有的Content - /// 要求content-type为application/x-www-form-urlencoded + /// 要求 content-type 为 application/x-www-form-urlencoded /// /// 名称 /// 值 /// - public async Task AddFormFieldAsync(string name, string? value) + public Task AddFormFieldAsync(string name, string? value) { var keyValue = new KeyValue(name, value); - await this.AddFormFieldAsync(Enumerable.Repeat(keyValue, 1)).ConfigureAwait(false); + return this.AddFormFieldAsync(Enumerable.Repeat(keyValue, 1)); } /// /// 添加字段到已有的Content - /// 要求content-type为application/x-www-form-urlencoded + /// 要求 content-type 为 application/x-www-form-urlencoded /// /// 键值对 /// @@ -52,7 +52,7 @@ public async Task AddFormFieldAsync(string name, string? value) /// /// 添加文本内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 名称 /// 文本 @@ -64,14 +64,14 @@ public void AddFormDataText(string name, string? value) /// /// 添加文本内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 键值对 public abstract void AddFormDataText(IEnumerable keyValues); /// /// 添加文件内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 文件流 /// 名称 diff --git a/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs b/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs index d6124b87..7580d578 100644 --- a/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs +++ b/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace WebApiClientCore @@ -13,6 +14,8 @@ public interface IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - ApiActionDescriptor CreateActionDescriptor(MethodInfo method, Type interfaceType); + ApiActionDescriptor CreateActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType); } } diff --git a/WebApiClientCore.Abstractions/IDataCollection.cs b/WebApiClientCore.Abstractions/IDataCollection.cs index 50997afd..c2eb0c50 100644 --- a/WebApiClientCore.Abstractions/IDataCollection.cs +++ b/WebApiClientCore.Abstractions/IDataCollection.cs @@ -13,7 +13,7 @@ public interface IDataCollection int Count { get; } /// - /// 返回是否包含指定的key + /// 返回是否包含指定的 key /// /// 键 /// @@ -28,7 +28,7 @@ public interface IDataCollection /// /// 读取指定的键并尝试转换为目标类型 - /// 失败则返回目标类型的default值 + /// 失败则返回目标类型的 default 值 /// /// /// 键 diff --git a/WebApiClientCore.Abstractions/IHttpApiActivator.cs b/WebApiClientCore.Abstractions/IHttpApiActivator.cs index 46137951..9e7a93bd 100644 --- a/WebApiClientCore.Abstractions/IHttpApiActivator.cs +++ b/WebApiClientCore.Abstractions/IHttpApiActivator.cs @@ -1,14 +1,12 @@ -namespace WebApiClientCore +using System.Diagnostics.CodeAnalysis; + +namespace WebApiClientCore { /// /// 定义THttpApi的实例创建器的接口 /// /// - public interface IHttpApiActivator< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi> + public interface IHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> { /// /// 创建THttpApi的代理实例 diff --git a/WebApiClientCore.Abstractions/Serialization/JsonConverters/JsonCompatibleConverter.cs b/WebApiClientCore.Abstractions/Serialization/JsonConverters/JsonCompatibleConverter.cs index 5c899e4b..e4c21bd3 100644 --- a/WebApiClientCore.Abstractions/Serialization/JsonConverters/JsonCompatibleConverter.cs +++ b/WebApiClientCore.Abstractions/Serialization/JsonConverters/JsonCompatibleConverter.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace WebApiClientCore.Serialization.JsonConverters { @@ -7,10 +8,20 @@ namespace WebApiClientCore.Serialization.JsonConverters /// public static class JsonCompatibleConverter { + private static JsonStringEnumConverter? stringEnumConverter; + /// /// 获取Enum类型反序列化兼容的转换器 /// - public static JsonConverter EnumReader { get; } = new JsonStringEnumConverter(); + public static JsonConverter EnumReader + { + [RequiresDynamicCode("JsonStringEnumConverter需要动态代码")] + get + { + stringEnumConverter ??= new JsonStringEnumConverter(); + return stringEnumConverter; + } + } /// /// 获取DateTime类型反序列化兼容的转换器 diff --git a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs index 0034bbbc..4342698b 100644 --- a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs +++ b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; @@ -13,7 +14,7 @@ namespace WebApiClientCore.Serialization public sealed class KeyValueSerializerOptions : KeyNamingOptions { /// - /// 包装的jsonOptions + /// 包装的 jsonOptions /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly JsonSerializerOptions jsonOptions; @@ -36,12 +37,17 @@ public JsonNamingPolicy? DictionaryKeyPolicy } /// - /// 获取或设置是否忽略null值 + /// 获取或设置是否忽略 null 值 /// public bool IgnoreNullValues { +#if NET5_0_OR_GREATER + get => jsonOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull; + set => jsonOptions.DefaultIgnoreCondition = value ? JsonIgnoreCondition.WhenWritingNull : JsonIgnoreCondition.Never; +#else get => jsonOptions.IgnoreNullValues; set => jsonOptions.IgnoreNullValues = value; +#endif } /// @@ -90,6 +96,8 @@ public KeyValueSerializerOptions() /// /// 目标类型 /// + [RequiresDynamicCode("Getting a converter for a type may require reflection which depends on runtime code generation.")] + [RequiresUnreferencedCode("Getting a converter for a type may require reflection which depends on unreferenced code.")] public JsonConverter GetConverter(Type typeToConvert) { return this.jsonOptions.GetConverter(typeToConvert); diff --git a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj index 4e50e9fc..db411581 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -2,11 +2,12 @@ enable - netstandard2.1;net5.0 - + netstandard2.1;net5.0;net8.0 + true + WebApiClientCore WebApiClientCore.Abstractions - $(TargetPath)\$(AssemblyName).xml + True WebApiClientCore的接口与抽象类型 WebApiClientCore的接口与抽象类型 @@ -15,7 +16,7 @@ Sign.snk - + diff --git a/WebApiClientCore.Analyzers/Descriptors.cs b/WebApiClientCore.Analyzers/Descriptors.cs index fd1f24bb..52cc29a1 100644 --- a/WebApiClientCore.Analyzers/Descriptors.cs +++ b/WebApiClientCore.Analyzers/Descriptors.cs @@ -28,7 +28,7 @@ static class Descriptors /// /// 非方法声明诊断描述器 /// - public static DiagnosticDescriptor NotMethodDefindedDescriptor { get; } + public static DiagnosticDescriptor NotMethodDefendedDescriptor { get; } = Create("WA1004", Resx.WA1004_title, Resx.WA1004_message); /// diff --git a/WebApiClientCore.Analyzers/HttpApiContext.cs b/WebApiClientCore.Analyzers/HttpApiContext.cs index 79e0c578..84a7bf89 100644 --- a/WebApiClientCore.Analyzers/HttpApiContext.cs +++ b/WebApiClientCore.Analyzers/HttpApiContext.cs @@ -84,14 +84,14 @@ public static bool TryParse(SyntaxNodeAnalysisContext syntaxNodeContext, out Htt return false; } - var ihttpApi = syntaxNodeContext.Compilation.GetTypeByMetadataName(IHttpApiTypeName); - if (ihttpApi == null) + var httpApi = syntaxNodeContext.Compilation.GetTypeByMetadataName(IHttpApiTypeName); + if (httpApi == null) { return false; } - var iapiAttribute = syntaxNodeContext.Compilation.GetTypeByMetadataName(IApiAttributeTypeName); - if (IsHttpApiInterface(@interface, ihttpApi, iapiAttribute) == false) + var apiAttribute = syntaxNodeContext.Compilation.GetTypeByMetadataName(IApiAttributeTypeName); + if (IsHttpApiInterface(@interface, httpApi, apiAttribute) == false) { return false; } @@ -105,27 +105,27 @@ public static bool TryParse(SyntaxNodeAnalysisContext syntaxNodeContext, out Htt } /// - /// 是否为http接口 + /// 是否为 http 接口 /// /// - /// - /// + /// + /// /// - private static bool IsHttpApiInterface(INamedTypeSymbol @interface, INamedTypeSymbol ihttpApi, INamedTypeSymbol? iapiAttribute) + private static bool IsHttpApiInterface(INamedTypeSymbol @interface, INamedTypeSymbol httpApi, INamedTypeSymbol? apiAttribute) { - if (@interface.AllInterfaces.Contains(ihttpApi)) + if (@interface.AllInterfaces.Contains(httpApi)) { return true; } - if (iapiAttribute == null) + if (apiAttribute == null) { return false; } return @interface.AllInterfaces.Append(@interface).Any(i => - HasAttribute(i, iapiAttribute) || i.GetMembers().OfType().Any(m => - HasAttribute(m, iapiAttribute) || m.Parameters.Any(p => HasAttribute(p, iapiAttribute)))); + HasAttribute(i, apiAttribute) || i.GetMembers().OfType().Any(m => + HasAttribute(m, apiAttribute) || m.Parameters.Any(p => HasAttribute(p, apiAttribute)))); } diff --git a/WebApiClientCore.Analyzers/HttpApiDiagnosticAnalyzer.cs b/WebApiClientCore.Analyzers/HttpApiDiagnosticAnalyzer.cs index 269227db..89899699 100644 --- a/WebApiClientCore.Analyzers/HttpApiDiagnosticAnalyzer.cs +++ b/WebApiClientCore.Analyzers/HttpApiDiagnosticAnalyzer.cs @@ -25,7 +25,7 @@ public override ImmutableArray SupportedDiagnostics Descriptors.AttributeDescriptor, Descriptors.ReturnTypeDescriptor, Descriptors.RefParameterDescriptor, - Descriptors.NotMethodDefindedDescriptor, + Descriptors.NotMethodDefendedDescriptor, Descriptors.GenericMethodDescriptor, Descriptors.UriAttributeDescriptor, Descriptors.ModifierDescriptor, @@ -70,7 +70,7 @@ private IEnumerable GetDiagnosticProviders(HttpApiCon yield return new CtorAttributeDiagnosticProvider(context); yield return new ReturnTypeDiagnosticProvider(context); yield return new RefParameterDiagnosticProvider(context); - yield return new NotMethodDefindedDiagnosticProvider(context); + yield return new NotMethodDefendedDiagnosticProvider(context); yield return new GenericMethodDiagnosticProvider(context); yield return new UriAttributeDiagnosticProvider(context); yield return new ModifierDiagnosticProvider(context); diff --git a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs index 4e51ed3f..944a4573 100644 --- a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs +++ b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs @@ -27,21 +27,20 @@ public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is HttpApiSyntaxReceiver receiver) { + // System.Diagnostics.Debugger.Launch(); var proxyClasses = receiver .GetHttpApiTypes(context.Compilation) .Select(i => new HttpApiProxyClass(i)) .Distinct() .ToArray(); - foreach (var proxyClass in proxyClasses) - { - context.AddSource(proxyClass.FileName, proxyClass.ToSourceText()); - } - if (proxyClasses.Length > 0) { - var initializer = new HttpApiProxyClassInitializer(context.Compilation, proxyClasses); - context.AddSource(initializer.FileName, initializer.ToSourceText()); + context.AddSource(HttpApiProxyClassInitializer.FileName, HttpApiProxyClassInitializer.ToSourceText()); + foreach (var proxyClass in proxyClasses) + { + context.AddSource(proxyClass.FileName, proxyClass.ToSourceText()); + } } } } diff --git a/WebApiClientCore.Analyzers/Providers/CtorAttributeDiagnosticProvider.cs b/WebApiClientCore.Analyzers/Providers/CtorAttributeDiagnosticProvider.cs index d08ef5a3..21ac9f21 100644 --- a/WebApiClientCore.Analyzers/Providers/CtorAttributeDiagnosticProvider.cs +++ b/WebApiClientCore.Analyzers/Providers/CtorAttributeDiagnosticProvider.cs @@ -58,7 +58,7 @@ private IEnumerable GetInterfaceCtorAttributes(INamedTypeSymbol @ { foreach (var attribute in @interface.GetAttributes()) { - if (this.CtorAttributeIsDefind(attribute, AttributeTargets.Interface) == false) + if (this.CtorAttributeIsDefend(attribute, AttributeTargets.Interface) == false) { yield return attribute; } @@ -75,7 +75,7 @@ private IEnumerable GetMethodCtorAttributes(IMethodSymbol methodS { foreach (var methodAttribute in methodSymbol.GetAttributes()) { - if (this.CtorAttributeIsDefind(methodAttribute, AttributeTargets.Method) == false) + if (this.CtorAttributeIsDefend(methodAttribute, AttributeTargets.Method) == false) { yield return methodAttribute; } @@ -85,7 +85,7 @@ private IEnumerable GetMethodCtorAttributes(IMethodSymbol methodS { foreach (var parameterAttribute in parameter.GetAttributes()) { - if (this.CtorAttributeIsDefind(parameterAttribute, AttributeTargets.Parameter) == false) + if (this.CtorAttributeIsDefend(parameterAttribute, AttributeTargets.Parameter) == false) { yield return parameterAttribute; } @@ -100,7 +100,7 @@ private IEnumerable GetMethodCtorAttributes(IMethodSymbol methodS /// /// 指定目标 /// - private bool CtorAttributeIsDefind(AttributeData attributeData, AttributeTargets targets) + private bool CtorAttributeIsDefend(AttributeData attributeData, AttributeTargets targets) { var ctorAttr = this.Context.AttributeCtorUsageAttribute; if (ctorAttr == null) diff --git a/WebApiClientCore.Analyzers/Providers/ModifierDiagnosticProvider.cs b/WebApiClientCore.Analyzers/Providers/ModifierDiagnosticProvider.cs index 3bbe0334..11bb58ac 100644 --- a/WebApiClientCore.Analyzers/Providers/ModifierDiagnosticProvider.cs +++ b/WebApiClientCore.Analyzers/Providers/ModifierDiagnosticProvider.cs @@ -23,8 +23,8 @@ public ModifierDiagnosticProvider(HttpApiContext context) public override IEnumerable CreateDiagnostics() { var syntax = this.Context.Syntax; - var isVisiable = syntax.Modifiers.Any(item => "public".Equals(item.ValueText)); - if (isVisiable == false) + var isVisible = syntax.Modifiers.Any(item => "public".Equals(item.ValueText)); + if (isVisible == false) { var location = syntax.Identifier.GetLocation(); yield return this.CreateDiagnostic(location); diff --git a/WebApiClientCore.Analyzers/Providers/NotMethodDefindedDiagnosticProvider.cs b/WebApiClientCore.Analyzers/Providers/NotMethodDefendedDiagnosticProvider.cs similarity index 87% rename from WebApiClientCore.Analyzers/Providers/NotMethodDefindedDiagnosticProvider.cs rename to WebApiClientCore.Analyzers/Providers/NotMethodDefendedDiagnosticProvider.cs index 5132c468..2cc9b389 100644 --- a/WebApiClientCore.Analyzers/Providers/NotMethodDefindedDiagnosticProvider.cs +++ b/WebApiClientCore.Analyzers/Providers/NotMethodDefendedDiagnosticProvider.cs @@ -7,20 +7,20 @@ namespace WebApiClientCore.Analyzers.Providers /// /// 表示非方法声明诊断器 /// - sealed class NotMethodDefindedDiagnosticProvider : HttpApiDiagnosticProvider + sealed class NotMethodDefendedDiagnosticProvider : HttpApiDiagnosticProvider { /// /// /// /// 获取诊断描述 /// /// - public override DiagnosticDescriptor Descriptor => Descriptors.NotMethodDefindedDescriptor; + public override DiagnosticDescriptor Descriptor => Descriptors.NotMethodDefendedDescriptor; /// /// 非方法声明诊断器 /// /// 上下文 - public NotMethodDefindedDiagnosticProvider(HttpApiContext context) + public NotMethodDefendedDiagnosticProvider(HttpApiContext context) : base(context) { } diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 876c2289..d704f0b1 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -8,49 +8,32 @@ namespace WebApiClientCore.Analyzers.SourceGenerator { /// - /// HttpApi代理类 + /// HttpApi代理类生成器 /// sealed class HttpApiProxyClass : IEquatable { - /// - /// 接口符号 - /// private readonly INamedTypeSymbol httpApi; private readonly string httpApiFullName; + private readonly string proxyClassName; - /// - /// 拦截器变量名 - /// - private readonly string apiInterceptorFieldName = $"apiInterceptor_{(uint)Environment.TickCount}"; - - /// - /// action执行器变量名 - /// - private readonly string actionInvokersFieldName = $"actionInvokers_{(uint)Environment.TickCount}"; + private const string ApiInterceptorFieldName = "_apiInterceptor"; + private const string ActionInvokersFieldName = "_actionInvokers"; /// /// 文件名 /// - public string FileName => this.httpApi.ToDisplayString(); - - /// - /// 命名空间 - /// - public string Namespace => $"WebApiClientCore.{this.httpApi.ContainingNamespace}"; - - /// - /// 类型名 - /// - public string ClassName => this.httpApi.Name; + public string FileName { get; } /// - /// HttpApi代理类 + /// HttpApi代理类生成器 /// /// public HttpApiProxyClass(INamedTypeSymbol httpApi) { this.httpApi = httpApi; this.httpApiFullName = GetFullName(httpApi); + this.proxyClassName = httpApi.ToDisplayString().Replace(".", "_"); + this.FileName = $"{nameof(HttpApiProxyClass)}.{proxyClassName}.g.cs"; } /// @@ -69,7 +52,6 @@ private static string GetFullName(ISymbol symbol) /// public SourceText ToSourceText() { - // System.Diagnostics.Debugger.Launch(); var code = this.ToString(); return SourceText.From(code, Encoding.UTF8); } @@ -82,38 +64,37 @@ public override string ToString() { var builder = new StringBuilder(); builder.AppendLine("#pragma warning disable"); - builder.AppendLine("using System;"); - builder.AppendLine($"namespace {this.Namespace}"); + builder.AppendLine($"namespace WebApiClientCore"); builder.AppendLine("{"); - - builder.AppendLine($"\t[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); - builder.AppendLine($"\t[global::System.Diagnostics.DebuggerTypeProxy(typeof({this.httpApiFullName}))]"); - builder.AppendLine($"\t[global::WebApiClientCore.HttpApiProxyClass(typeof({this.httpApiFullName}))]"); - builder.AppendLine($"\tpartial class {this.ClassName}:{this.httpApiFullName}"); + builder.AppendLine($"\tpartial class {nameof(HttpApiProxyClass)}"); builder.AppendLine("\t{"); - - builder.AppendLine($"\t\tprivate readonly global::WebApiClientCore.IHttpApiInterceptor {this.apiInterceptorFieldName};"); - builder.AppendLine($"\t\tprivate readonly global::WebApiClientCore.ApiActionInvoker[] {this.actionInvokersFieldName};"); - - builder.AppendLine($"\t\tpublic {this.httpApi.Name}(global::WebApiClientCore.IHttpApiInterceptor apiInterceptor, global::WebApiClientCore.ApiActionInvoker[] actionInvokers)"); + builder.AppendLine($"\t\t[global::WebApiClientCore.HttpApiProxyClass(typeof({this.httpApiFullName}))]"); + builder.AppendLine($"\t\t[global::System.Diagnostics.DebuggerTypeProxy(typeof({this.httpApiFullName}))]"); + builder.AppendLine($"\t\tsealed partial class {this.proxyClassName} : {this.httpApiFullName}"); builder.AppendLine("\t\t{"); - builder.AppendLine($"\t\t\tthis.{this.apiInterceptorFieldName} = apiInterceptor;"); - builder.AppendLine($"\t\t\tthis.{this.actionInvokersFieldName} = actionInvokers;"); - builder.AppendLine("\t\t}"); + + builder.AppendLine($"\t\t\tprivate readonly global::WebApiClientCore.IHttpApiInterceptor {ApiInterceptorFieldName};"); + builder.AppendLine($"\t\t\tprivate readonly global::WebApiClientCore.ApiActionInvoker[] {ActionInvokersFieldName};"); + builder.AppendLine(); + builder.AppendLine($"\t\t\tpublic {this.proxyClassName}(global::WebApiClientCore.IHttpApiInterceptor apiInterceptor, global::WebApiClientCore.ApiActionInvoker[] actionInvokers)"); + builder.AppendLine("\t\t\t{"); + builder.AppendLine($"\t\t\t\tthis.{ApiInterceptorFieldName} = apiInterceptor;"); + builder.AppendLine($"\t\t\t\tthis.{ActionInvokersFieldName} = actionInvokers;"); + builder.AppendLine("\t\t\t}"); builder.AppendLine(); var index = 0; - foreach (var interfaceType in this.httpApi.AllInterfaces.Append(httpApi)) + foreach (var declaringType in this.httpApi.AllInterfaces.Append(httpApi)) { - foreach (var method in interfaceType.GetMembers().OfType()) + foreach (var method in declaringType.GetMembers().OfType()) { - var methodCode = this.BuildMethod(interfaceType, method, index); + var methodCode = this.BuildMethod(declaringType, method, index); builder.AppendLine(methodCode); index += 1; } } - + builder.AppendLine("\t\t}"); builder.AppendLine("\t}"); builder.AppendLine("}"); builder.AppendLine("#pragma warning restore"); @@ -124,26 +105,27 @@ public override string ToString() /// /// 构建方法 /// - /// + /// /// /// /// - private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, int index) + private string BuildMethod(INamedTypeSymbol declaringType, IMethodSymbol method, int index) { var builder = new StringBuilder(); - var parametersString = string.Join(",", method.Parameters.Select(item => $"{GetFullName(item.Type)} {item.Name}")); - var parameterNamesString = string.Join(",", method.Parameters.Select(item => item.Name)); - var paremterArrayString = string.IsNullOrEmpty(parameterNamesString) + var parametersString = string.Join(", ", method.Parameters.Select((item, i) => $"{GetFullName(item.Type)} p{i}")); + var parameterNamesString = string.Join(", ", method.Parameters.Select((item, i) => $"p{i}")); + var parameterArrayString = string.IsNullOrEmpty(parameterNamesString) ? "global::System.Array.Empty()" : $"new global::System.Object[] {{ {parameterNamesString} }}"; - var methodName = $"\"{interfaceType.ToDisplayString()}.{method.Name}\""; var returnTypeString = GetFullName(method.ReturnType); - builder.AppendLine($"\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, {methodName})]"); - builder.AppendLine($"\t\t{returnTypeString} {GetFullName(interfaceType)}.{method.Name}( {parametersString} )"); - builder.AppendLine("\t\t{"); - builder.AppendLine($"\t\t\treturn ({returnTypeString})this.{this.apiInterceptorFieldName}.Intercept(this.{this.actionInvokersFieldName}[{index}], {paremterArrayString});"); - builder.AppendLine("\t\t}"); + var declaringTypeString = GetFullName(declaringType); + + builder.AppendLine($"\t\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, \"{method.Name}\", typeof({declaringTypeString}))]"); + builder.AppendLine($"\t\t\t{returnTypeString} {declaringTypeString}.{method.Name}({parametersString})"); + builder.AppendLine("\t\t\t{"); + builder.AppendLine($"\t\t\t\treturn ({returnTypeString})this.{ApiInterceptorFieldName}.Intercept(this.{ActionInvokersFieldName}[{index}], {parameterArrayString});"); + builder.AppendLine("\t\t\t}"); return builder.ToString(); } @@ -152,9 +134,9 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, /// /// /// - public bool Equals(HttpApiProxyClass other) + public bool Equals(HttpApiProxyClass? other) { - return this.FileName == other.FileName; + return other != null && this.FileName == other.FileName; } /// @@ -162,13 +144,9 @@ public bool Equals(HttpApiProxyClass other) /// /// /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (obj is HttpApiProxyClass builder) - { - return this.Equals(builder); - } - return false; + return obj is HttpApiProxyClass other && this.Equals(other); } /// diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs index 4ba5253a..dfef221f 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs @@ -1,78 +1,48 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using System.Collections.Generic; +using Microsoft.CodeAnalysis.Text; using System.Text; namespace WebApiClientCore.Analyzers.SourceGenerator { /// - /// HttpApi代理类初始化器 + /// HttpApiProxyClass的静态类初始器 /// - sealed class HttpApiProxyClassInitializer + static class HttpApiProxyClassInitializer { - private readonly Compilation compilation; - private readonly IEnumerable proxyClasses; - /// /// 文件名 /// - public string FileName => $"{nameof(HttpApiProxyClassInitializer)}.cs"; - - /// - /// HttpApi代理类初始化器 - /// - /// - /// - public HttpApiProxyClassInitializer(Compilation compilation, IEnumerable proxyClasses) - { - this.compilation = compilation; - this.proxyClasses = proxyClasses; - } + public static string FileName => $"{nameof(HttpApiProxyClass)}.g.cs"; /// /// 转换为SourceText /// /// - public SourceText ToSourceText() + public static SourceText ToSourceText() { - var code = this.ToString(); + var code = $$""" + #pragma warning disable + using System; + namespace WebApiClientCore + { + /// HttpApi代理类 + [global::System.Reflection.Obfuscation(Exclude = true)] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static partial class {{nameof(HttpApiProxyClass)}} + { + #if NET5_0_OR_GREATER + /// 初始化代理类 + [global::System.Runtime.CompilerServices.ModuleInitializer] + [global::System.Diagnostics.CodeAnalysis.DynamicDependency(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All, typeof(global::WebApiClientCore.{{nameof(HttpApiProxyClass)}}))] + public static void Initialize() + { + } + #endif + } + } + #pragma warning restore + """; return SourceText.From(code, Encoding.UTF8); } - - public override string ToString() - { - var builder = new StringBuilder(); - builder.AppendLine("#if NET5_0_OR_GREATER"); - builder.AppendLine("#pragma warning disable"); - builder.AppendLine($"namespace WebApiClientCore"); - builder.AppendLine("{"); - builder.AppendLine(" /// 动态依赖初始化器"); - builder.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); - builder.AppendLine($" static partial class {nameof(HttpApiProxyClassInitializer)}"); - builder.AppendLine(" {"); - - builder.AppendLine($""" - /// - /// 注册程序集{compilation.AssemblyName}的所有动态依赖 - /// 避免程序集在裁剪时裁剪掉由SourceGenerator生成的代理类 - /// - """); - - builder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]"); - foreach (var item in this.proxyClasses) - { - builder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.DynamicDependency(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All, typeof(global::{item.Namespace}.{item.ClassName}))]"); - } - - builder.AppendLine(" public static void Initialize()"); - builder.AppendLine(" {"); - builder.AppendLine(" }"); - builder.AppendLine(" }"); - builder.AppendLine("}"); - builder.AppendLine("#pragma warning restore"); - builder.AppendLine("#endif"); - return builder.ToString(); - } - } } diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiSyntaxReceiver.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiSyntaxReceiver.cs index d6851ed0..fd47ef30 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiSyntaxReceiver.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiSyntaxReceiver.cs @@ -41,17 +41,17 @@ void ISyntaxReceiver.OnVisitSyntaxNode(SyntaxNode syntaxNode) /// public IEnumerable GetHttpApiTypes(Compilation compilation) { - var ihttpApi = compilation.GetTypeByMetadataName(IHttpApiTypeName); - if (ihttpApi == null) + var httpApi = compilation.GetTypeByMetadataName(IHttpApiTypeName); + if (httpApi == null) { yield break; } - var iapiAttribute = compilation.GetTypeByMetadataName(IApiAttributeTypeName); + var apiAttribute = compilation.GetTypeByMetadataName(IApiAttributeTypeName); foreach (var interfaceSyntax in this.interfaceSyntaxList) { var @interface = compilation.GetSemanticModel(interfaceSyntax.SyntaxTree).GetDeclaredSymbol(interfaceSyntax); - if (@interface != null && IsHttpApiInterface(@interface, ihttpApi, iapiAttribute)) + if (@interface != null && IsHttpApiInterface(@interface, httpApi, apiAttribute)) { yield return @interface; } @@ -60,27 +60,27 @@ public IEnumerable GetHttpApiTypes(Compilation compilation) /// - /// 是否为http接口 + /// 是否为 http 接口 /// /// - /// - /// + /// + /// /// - private static bool IsHttpApiInterface(INamedTypeSymbol @interface, INamedTypeSymbol ihttpApi, INamedTypeSymbol? iapiAttribute) + private static bool IsHttpApiInterface(INamedTypeSymbol @interface, INamedTypeSymbol httpApi, INamedTypeSymbol? apiAttribute) { - if (@interface.AllInterfaces.Contains(ihttpApi)) + if (@interface.AllInterfaces.Contains(httpApi)) { return true; } - if (iapiAttribute == null) + if (apiAttribute == null) { return false; } return @interface.AllInterfaces.Append(@interface).Any(i => - HasAttribute(i, iapiAttribute) || i.GetMembers().OfType().Any(m => - HasAttribute(m, iapiAttribute) || m.Parameters.Any(p => HasAttribute(p, iapiAttribute)))); + HasAttribute(i, apiAttribute) || i.GetMembers().OfType().Any(m => + HasAttribute(m, apiAttribute) || m.Parameters.Any(p => HasAttribute(p, apiAttribute)))); } diff --git a/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj b/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj index 254ad3d2..55ddda26 100644 --- a/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj +++ b/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj @@ -3,7 +3,7 @@ enable netstandard2.0 - $(TargetPath)\$(AssemblyName).xml + True false false true diff --git a/WebApiClientCore.Benchmarks/Buffers/Benchmark.cs b/WebApiClientCore.Benchmarks/Buffers/Benchmark.cs deleted file mode 100644 index 6bbf9c3b..00000000 --- a/WebApiClientCore.Benchmarks/Buffers/Benchmark.cs +++ /dev/null @@ -1,21 +0,0 @@ -using BenchmarkDotNet.Attributes; -using WebApiClientCore.Internals; - -namespace WebApiClientCore.Benchmarks.Buffers -{ - [InProcess] - public class Benchmark : IBenchmark - { - [Benchmark] - public void Rent() - { - using (new RecyclableBufferWriter()) { } - } - - [Benchmark] - public void New() - { - _ = new byte[1024]; - } - } -} diff --git a/WebApiClientCore.Benchmarks/CreateInstances/Benchmarks.cs b/WebApiClientCore.Benchmarks/CreateInstances/Benchmarks.cs deleted file mode 100644 index 3ea9ac9f..00000000 --- a/WebApiClientCore.Benchmarks/CreateInstances/Benchmarks.cs +++ /dev/null @@ -1,34 +0,0 @@ -using BenchmarkDotNet.Attributes; -using System; -using WebApiClientCore.Internals; - -namespace WebApiClientCore.Benchmarks.CreateInstances -{ - [InProcess] - public class Benchmarks : IBenchmark - { - private readonly Func ctor = LambdaUtil.CreateCtorFunc(typeof(Model)); - - [Benchmark] - public void ActivatorCreate() - { - typeof(Model).CreateInstance(1); - } - - [Benchmark] - public void LabdaCreate() - { - ctor.Invoke(1); - } - - public class Model - { - private readonly int value; - - public Model(int value) - { - this.value = value; - } - } - } -} diff --git a/WebApiClientCore.Benchmarks/IBenchmark.cs b/WebApiClientCore.Benchmarks/IBenchmark.cs deleted file mode 100644 index 3ceb6c72..00000000 --- a/WebApiClientCore.Benchmarks/IBenchmark.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WebApiClientCore.Benchmarks -{ - interface IBenchmark - { - } -} diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index e2cac205..4969a42e 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Running; -using System; -using System.Linq; +using WebApiClientCore.Benchmarks.Requests; namespace WebApiClientCore.Benchmarks { @@ -8,15 +7,11 @@ class Program { static void Main(string[] args) { - var benchmarkTypes = typeof(Program).Assembly.GetTypes() - .Where(item => typeof(IBenchmark).IsAssignableFrom(item)) - .Where(item => item.IsAbstract == false && item.IsClass); - - foreach (var item in benchmarkTypes) - { - BenchmarkRunner.Run(item); - } - Console.ReadLine(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } } } diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 6d1a0283..8c6bbb82 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -2,56 +2,98 @@ using Microsoft.Extensions.DependencyInjection; using Refit; using System; +using System.IO; +using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; using System.Threading.Tasks; namespace WebApiClientCore.Benchmarks.Requests { [InProcess] [MemoryDiagnoser] - public abstract class Benchmark : IBenchmark + public abstract class Benchmark { protected IServiceProvider ServiceProvider { get; set; } - [GlobalSetup] - public async Task SetupAsync() + public void GlobalSetup() { var services = new ServiceCollection(); + services + .AddWebApiClient() + .ConfigureHttpApi(o => + { + o.UseLogging = false; + o.UseParameterPropertyValidate = false; + o.UseReturnValuePropertyValidate = false; + }); services - .AddHttpClient(typeof(HttpClient).FullName) - .AddHttpMessageHandler(() => new MockResponseHandler()); + .AddHttpApi() + .AddHttpMessageHandler(() => new JsonResponseHandler()) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); + + services + .AddHttpApi() + .AddHttpMessageHandler(() => new XmlResponseHandler()) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services - .AddHttpApi(o => + .AddRefitClient(new RefitSettings { - o.UseParameterPropertyValidate = false; - o.UseReturnValuePropertyValidate = false; + ContentSerializer = new SystemTextJsonContentSerializer() }) - .AddHttpMessageHandler(() => new MockResponseHandler()) + .AddHttpMessageHandler(() => new JsonResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services - .AddRefitClient(new RefitSettings + .AddRefitClient(new RefitSettings { - Buffered = true, + ContentSerializer = new XmlContentSerializer() }) - .AddHttpMessageHandler(() => new MockResponseHandler()) + .AddHttpMessageHandler(() => new XmlResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); this.ServiceProvider = services.BuildServiceProvider(); - using var scope = this.ServiceProvider.CreateScope(); - - var core = scope.ServiceProvider.GetService(); - var refit = scope.ServiceProvider.GetService(); + // 服务显式加载预热 + this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); + } - await core.GetAsyc("id"); - await core.PostJsonAsync(new Model { }); + private class JsonResponseHandler : DelegatingHandler + { + private static readonly MediaTypeHeaderValue applicationJson = MediaTypeHeaderValue.Parse("application/json"); + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false), User.Utf8Json.Length); + content.Headers.ContentType = applicationJson; + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = content + }; + return Task.FromResult(response); + } + } - await refit.GetAsyc("id"); - await refit.PostJsonAsync(new Model { }); + private class XmlResponseHandler : DelegatingHandler + { + private static readonly MediaTypeHeaderValue applicationXml = MediaTypeHeaderValue.Parse("application/xml"); + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var content = new StringContent(User.XmlString, Encoding.UTF8); + content.Headers.ContentType = applicationXml; + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = content + }; + return Task.FromResult(response); + } } } } diff --git a/WebApiClientCore.Benchmarks/Requests/ByteArrayJsonContent.cs b/WebApiClientCore.Benchmarks/Requests/ByteArrayJsonContent.cs deleted file mode 100644 index a9405191..00000000 --- a/WebApiClientCore.Benchmarks/Requests/ByteArrayJsonContent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Net.Http; -using System.Net.Http.Headers; - -namespace WebApiClientCore.Benchmarks.Requests -{ - /// - /// 表示http请求的json内容 - /// - class ByteArrayJsonContent : ByteArrayContent - { - /// - /// http请求的json内容 - /// - /// utf8 json - public ByteArrayJsonContent(byte[] json) - : base(json) - { - this.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - } - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs deleted file mode 100644 index f44ffab3..00000000 --- a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs +++ /dev/null @@ -1,58 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; - -namespace WebApiClientCore.Benchmarks.Requests -{ - /// - /// 跳过真实的http请求环节的模拟Get请求 - /// - [MemoryDiagnoser] - public class GetBenchmark : Benchmark - { - /// - /// 使用原生HttpClient请求 - /// - /// - [Benchmark] - public async Task HttpClient_GetAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var httpClient = scope.ServiceProvider.GetRequiredService().CreateClient(typeof(HttpClient).FullName); - - var id = "id"; - var request = new HttpRequestMessage(HttpMethod.Get, $"http://webapiclient.com/{id}"); - var response = await httpClient.SendAsync(request); - var json = await response.Content.ReadAsUtf8ByteArrayAsync(); - return JsonSerializer.Deserialize(json); - } - - - /// - /// 使用WebApiClientCore请求 - /// - /// - [Benchmark(Baseline = true)] - public async Task WebApiClientCore_GetAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await banchmarkApi.GetAsyc(id: "id"); - } - - - /// - /// Refit的Get请求 - /// - /// - [Benchmark] - public async Task Refit_GetAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await banchmarkApi.GetAsyc(id: "id"); - } - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs new file mode 100644 index 00000000..79b2f4bf --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public class HttpGetBenchmark : Benchmark + { + [Benchmark(Baseline = true)] + public async Task WebApiClientCore_GetAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + await benchmarkApi.GetAsync(id: "id001"); + } + + [Benchmark] + public async Task Refit_GetAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + await benchmarkApi.GetAsync(id: "id001"); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs new file mode 100644 index 00000000..45ccebb7 --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public class HttpGetJsonBenchmark : Benchmark + { + [Benchmark(Baseline = true)] + public async Task WebApiClientCore_GetJsonAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.GetJsonAsync(id: "id001"); + } + + [Benchmark] + public async Task Refit_GetJsonAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.GetJsonAsync(id: "id001"); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs new file mode 100644 index 00000000..9ccaa97d --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs @@ -0,0 +1,26 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public class HttpPostJsonBenchmark : Benchmark + { + [Benchmark(Baseline = true)] + public async Task WebApiClientCore_PostJsonAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PostJsonAsync(User.Instance); + } + + + [Benchmark] + public async Task Refit_PostJsonAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PostJsonAsync(User.Instance); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs new file mode 100644 index 00000000..f282158a --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public class HttpPostXmlBenchmark : Benchmark + { + [Benchmark(Baseline = true)] + public async Task WebApiClientCore_PostXmlAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PostXmlAsync(User.Instance); + } + + [Benchmark] + public async Task Refit_PostXmlAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PostXmlAsync(User.Instance); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs new file mode 100644 index 00000000..9672d6c1 --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public class HttpPutFormBenchmark : Benchmark + { + [Benchmark(Baseline = true)] + public async Task WebApiClientCore_PutFormAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PutFormAsync(id: "id001", User.Instance); + } + + [Benchmark] + public async Task Refit_PutFormAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PutFormAsync(id: "id001", User.Instance); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs deleted file mode 100644 index 868da657..00000000 --- a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Refit; -using System.Threading.Tasks; - -namespace WebApiClientCore.Benchmarks.Requests -{ - public interface IRefitApi - { - [Get("/benchmarks/{id}")] - Task GetAsyc(string id); - - [Post("/benchmarks")] - Task PostJsonAsync([Body(BodySerializationMethod.Serialized)]Model model); - - [Put("/benchmarks/{id}")] - Task PutFormAsync(string id, [Body(BodySerializationMethod.UrlEncoded)]Model model); - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/IRefitJsonApi.cs b/WebApiClientCore.Benchmarks/Requests/IRefitJsonApi.cs new file mode 100644 index 00000000..2e094ec0 --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/IRefitJsonApi.cs @@ -0,0 +1,20 @@ +using Refit; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public interface IRefitJsonApi + { + [Get("/benchmarks/{id}")] + Task GetAsync(string id); + + [Get("/benchmarks/{id}")] + Task GetJsonAsync(string id); + + [Post("/benchmarks")] + Task PostJsonAsync([Body(BodySerializationMethod.Serialized)] User model); + + [Put("/benchmarks/{id}")] + Task PutFormAsync(string id, [Body(BodySerializationMethod.UrlEncoded)] User model); + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/IRefitXmlApi.cs b/WebApiClientCore.Benchmarks/Requests/IRefitXmlApi.cs new file mode 100644 index 00000000..21d1579e --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/IRefitXmlApi.cs @@ -0,0 +1,11 @@ +using Refit; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public interface IRefitXmlApi + { + [Post("/benchmarks")] + Task PostXmlAsync([Body(BodySerializationMethod.Serialized)] User model); + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs deleted file mode 100644 index fd118877..00000000 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using WebApiClientCore.Attributes; - -namespace WebApiClientCore.Benchmarks.Requests -{ - public interface IWebApiClientCoreApi - { - [HttpGet("/benchmarks/{id}")] - Task GetAsyc(string id); - - [HttpPost("/benchmarks")] - Task PostJsonAsync([JsonContent] Model model); - - [HttpPut("/benchmarks/{id}")] - Task PutFormAsync(string id, [FormContent] Model model); - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs new file mode 100644 index 00000000..23bc02a4 --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using WebApiClientCore.Attributes; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public interface IWebApiClientCoreJsonApi + { + [HttpGet("/benchmarks/{id}")] + Task GetAsync(string id); + + [HttpGet("/benchmarks/{id}")] + Task GetJsonAsync(string id); + + [HttpPost("/benchmarks")] + Task PostJsonAsync([JsonContent] User model); + + [HttpPut("/benchmarks/{id}")] + Task PutFormAsync(string id, [FormContent] User model); + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs new file mode 100644 index 00000000..46663372 --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using WebApiClientCore.Attributes; + +namespace WebApiClientCore.Benchmarks.Requests +{ + public interface IWebApiClientCoreXmlApi + { + [HttpPost("/benchmarks")] + Task PostXmlAsync([XmlContent] User model); + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs b/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs deleted file mode 100644 index c12b3497..00000000 --- a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace WebApiClientCore.Benchmarks.Requests -{ - /// - /// 无真实http请求的Handler - /// - class MockResponseHandler : DelegatingHandler - { - private readonly byte[] json; - - public MockResponseHandler() - { - var model = new Model { A = "A", B = 2, C = 3d }; - this.json = JsonSerializer.SerializeToUtf8Bytes(model); - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayJsonContent(this.json) }; - return Task.FromResult(response); - } - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/Model.cs b/WebApiClientCore.Benchmarks/Requests/Model.cs deleted file mode 100644 index 16882184..00000000 --- a/WebApiClientCore.Benchmarks/Requests/Model.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace WebApiClientCore.Benchmarks.Requests -{ - public class Model - { - public string A { get; set; } - - public int B { get; set; } - - public double C { get; set; } - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs deleted file mode 100644 index bec2b14c..00000000 --- a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs +++ /dev/null @@ -1,60 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; - -namespace WebApiClientCore.Benchmarks.Requests -{ - /// - /// 跳过真实的http请求环节的模拟Post json请求 - /// - [MemoryDiagnoser] - public class PostJsonBenchmark : Benchmark - { - /// - /// 使用原生HttpClient请求 - /// - /// - [Benchmark] - public async Task HttpClient_PostJsonAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var httpClient = scope.ServiceProvider.GetRequiredService().CreateClient(typeof(HttpClient).FullName); - - var input = new Model { A = "a" }; - var json = JsonSerializer.SerializeToUtf8Bytes(input); - var request = new HttpRequestMessage(HttpMethod.Post, $"http://webapiclient.com/") - { - Content = new ByteArrayJsonContent(json) - }; - - var response = await httpClient.SendAsync(request); - json = await response.Content.ReadAsUtf8ByteArrayAsync(); - return JsonSerializer.Deserialize(json); - } - - /// - /// 使用WebApiClientCore请求 - /// - /// - [Benchmark(Baseline = true)] - public async Task WebApiClientCore_PostJsonAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await banchmarkApi.PostJsonAsync(input); - } - - - [Benchmark] - public async Task Refit_PostJsonAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await banchmarkApi.PostJsonAsync(input); - } - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs deleted file mode 100644 index 67bd3c19..00000000 --- a/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs +++ /dev/null @@ -1,36 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.DependencyInjection; -using System.Threading.Tasks; - -namespace WebApiClientCore.Benchmarks.Requests -{ - /// - /// 跳过真实的http请求环节的模拟Post表单请求 - /// - [MemoryDiagnoser] - public class PutFormBenchmark : Benchmark - { - /// - /// 使用WebApiClientCore请求 - /// - /// - [Benchmark(Baseline = true)] - public async Task WebApiClientCore_PutFormAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await banchmarkApi.PutFormAsync("id001", input); - } - - - [Benchmark] - public async Task Refit_PutFormAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await banchmarkApi.PutFormAsync("id001", input); - } - } -} diff --git a/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs b/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs deleted file mode 100644 index e5657c32..00000000 --- a/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs +++ /dev/null @@ -1,32 +0,0 @@ -using BenchmarkDotNet.Attributes; -using System.Text.RegularExpressions; - -namespace WebApiClientCore.Benchmarks.StringReplaces -{ - [InProcess] - [MemoryDiagnoser] - public class Benchmark : IBenchmark - { - private readonly string str = "WebApiClientCore.Benchmarks.StringReplaces.WebApiClientCore"; - private readonly string pattern = "core"; - private readonly string replacement = "CORE"; - - [Benchmark] - public void ReplaceByRegexNew() - { - new Regex(pattern, RegexOptions.IgnoreCase).Replace(str, replacement); - } - - [Benchmark] - public void ReplaceByRegexStatic() - { - Regex.Replace(str, pattern, replacement, RegexOptions.IgnoreCase); - } - - [Benchmark] - public void ReplaceByCutomSpan() - { - str.RepaceIgnoreCase(pattern, replacement, out var _); - } - } -} diff --git a/WebApiClientCore.Benchmarks/User.cs b/WebApiClientCore.Benchmarks/User.cs new file mode 100644 index 00000000..3f704601 --- /dev/null +++ b/WebApiClientCore.Benchmarks/User.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using WebApiClientCore.Serialization; + +namespace WebApiClientCore.Benchmarks +{ + public class User + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("bio")] + public string Bio { get; set; } + + [JsonPropertyName("followers")] + public int Followers { get; set; } + + [JsonPropertyName("following")] + public int Following { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + + public static User Instance { get; } + public static byte[] Utf8Json { get; } + public static string XmlString { get; set; } + + static User() + { + Utf8Json = File.ReadAllBytes("user.json"); + Instance = JsonSerializer.Deserialize(Utf8Json); + XmlString = XmlSerializer.Serialize(Instance, null); + } + } +} diff --git a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj index dbbb25f8..fbe94c46 100644 --- a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj +++ b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj @@ -3,6 +3,7 @@ net8.0 Exe + true true Sign.snk false @@ -12,10 +13,18 @@ + + + + + + + Always + diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md deleted file mode 100644 index 21b1f507..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md +++ /dev/null @@ -1,14 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4412/22H2/2022Update) -Intel Core i3-4150 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 2 physical cores - [Host] : .NET 8.0.4, X64 NativeAOT AVX2 - -Job=InProcess Toolchain=InProcessEmitToolchain - -``` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|-------------------------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| HttpClient_GetAsync | 2.203 μs | 0.0430 μs | 0.0574 μs | 0.42 | 0.01 | 1.3237 | 2.03 KB | 0.51 | -| WebApiClientCore_GetAsync | 5.245 μs | 0.1027 μs | 0.1142 μs | 1.00 | 0.00 | 2.6169 | 4.02 KB | 1.00 | -| Refit_GetAsync | 12.336 μs | 0.2447 μs | 0.6615 μs | 2.37 | 0.13 | 3.4790 | 5.34 KB | 1.33 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md new file mode 100644 index 00000000..efe7b7ff --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md @@ -0,0 +1,13 @@ +``` + +BenchmarkDotNet v0.13.12, CentOS Linux 7 (Core) +Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores + [Host] : .NET 8.0.4, X64 NativeAOT AVX + +Job=InProcess Toolchain=InProcessEmitToolchain + +``` +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_GetAsync | 5.558 μs | 0.1094 μs | 0.1384 μs | 1.00 | 0.00 | 0.3357 | 3.45 KB | 1.00 | +| Refit_GetAsync | 14.494 μs | 0.2764 μs | 0.3394 μs | 2.61 | 0.10 | 0.4883 | 5.18 KB | 1.50 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report-github.md new file mode 100644 index 00000000..ba3476ec --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report-github.md @@ -0,0 +1,13 @@ +``` + +BenchmarkDotNet v0.13.12, CentOS Linux 7 (Core) +Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores + [Host] : .NET 8.0.4, X64 NativeAOT AVX + +Job=InProcess Toolchain=InProcessEmitToolchain + +``` +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------ |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_GetJsonAsync | 11.31 μs | 0.225 μs | 0.322 μs | 1.00 | 0.00 | 0.4120 | 4.3 KB | 1.00 | +| Refit_GetJsonAsync | 29.03 μs | 0.575 μs | 0.788 μs | 2.57 | 0.09 | 0.5493 | 5.67 KB | 1.32 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report-github.md new file mode 100644 index 00000000..cac90723 --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report-github.md @@ -0,0 +1,13 @@ +``` + +BenchmarkDotNet v0.13.12, CentOS Linux 7 (Core) +Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores + [Host] : .NET 8.0.4, X64 NativeAOT AVX + +Job=InProcess Toolchain=InProcessEmitToolchain + +``` +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_PostJsonAsync | 11.26 μs | 0.221 μs | 0.331 μs | 1.00 | 0.00 | 0.4120 | 4.23 KB | 1.00 | +| Refit_PostJsonAsync | 26.16 μs | 0.510 μs | 0.663 μs | 2.32 | 0.08 | 0.5798 | 6.08 KB | 1.44 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report-github.md new file mode 100644 index 00000000..5f090cda --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report-github.md @@ -0,0 +1,13 @@ +``` + +BenchmarkDotNet v0.13.12, CentOS Linux 7 (Core) +Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores + [Host] : .NET 8.0.4, X64 NativeAOT AVX + +Job=InProcess Toolchain=InProcessEmitToolchain + +``` +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------------------------------ |---------:|---------:|---------:|---------:|------:|--------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_PostXmlAsync | 47.97 μs | 0.943 μs | 2.009 μs | 47.11 μs | 1.00 | 0.00 | 3.4180 | 0.1221 | 35.48 KB | 1.00 | +| Refit_PostXmlAsync | 57.06 μs | 0.948 μs | 0.740 μs | 56.87 μs | 1.21 | 0.02 | 14.0381 | 2.3193 | 144.38 KB | 4.07 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report-github.md new file mode 100644 index 00000000..7e1f4f5f --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report-github.md @@ -0,0 +1,13 @@ +``` + +BenchmarkDotNet v0.13.12, CentOS Linux 7 (Core) +Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores + [Host] : .NET 8.0.4, X64 NativeAOT AVX + +Job=InProcess Toolchain=InProcessEmitToolchain + +``` +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------ |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_PutFormAsync | 19.94 μs | 0.394 μs | 0.679 μs | 19.62 μs | 1.00 | 0.00 | 0.5493 | 5.7 KB | 1.00 | +| Refit_PutFormAsync | 79.90 μs | 1.551 μs | 2.321 μs | 78.62 μs | 3.98 | 0.17 | 1.0986 | 11.57 KB | 2.03 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md deleted file mode 100644 index fee92064..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md +++ /dev/null @@ -1,14 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4412/22H2/2022Update) -Intel Core i3-4150 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 2 physical cores - [Host] : .NET 8.0.4, X64 NativeAOT AVX2 - -Job=InProcess Toolchain=InProcessEmitToolchain - -``` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|------------------------------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| HttpClient_PostJsonAsync | 2.760 μs | 0.0294 μs | 0.0246 μs | 0.48 | 0.01 | 1.5068 | 2.31 KB | 0.55 | -| WebApiClientCore_PostJsonAsync | 5.712 μs | 0.0614 μs | 0.0512 μs | 1.00 | 0.00 | 2.7237 | 4.17 KB | 1.00 | -| Refit_PostJsonAsync | 13.246 μs | 0.0457 μs | 0.0382 μs | 2.32 | 0.02 | 3.9215 | 6.02 KB | 1.44 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md deleted file mode 100644 index 1082bc83..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md +++ /dev/null @@ -1,13 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4412/22H2/2022Update) -Intel Core i3-4150 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 2 physical cores - [Host] : .NET 8.0.4, X64 NativeAOT AVX2 - -Job=InProcess Toolchain=InProcessEmitToolchain - -``` -| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|------------------------------ |----------:|----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_PutFormAsync | 8.689 μs | 0.1715 μs | 0.3136 μs | 8.733 μs | 1.00 | 0.00 | 3.2501 | 5 KB | 1.00 | -| Refit_PutFormAsync | 20.598 μs | 0.4215 μs | 1.2429 μs | 21.112 μs | 2.37 | 0.17 | 4.5776 | 7.05 KB | 1.41 | diff --git a/WebApiClientCore.Benchmarks/user.json b/WebApiClientCore.Benchmarks/user.json new file mode 100644 index 00000000..c5c4b4c4 --- /dev/null +++ b/WebApiClientCore.Benchmarks/user.json @@ -0,0 +1,8 @@ +{ + "id": 253, + "name": "Namee3a23814-bfe9-4d4b-96db-8fc95d209ea8", + "bio": "Biof413d158-7ca7-4b1b-9073-565b3621bb83", + "followers": 154, + "following": 136, + "url": "Url70f46596-f86f-4e82-900d-0f07d7dc468c" +} \ No newline at end of file diff --git a/WebApiClientCore.Extensions.JsonRpc/Attributes/JsonRpcMethodAttribute.cs b/WebApiClientCore.Extensions.JsonRpc/Attributes/JsonRpcMethodAttribute.cs index 524c32ef..df998fcb 100644 --- a/WebApiClientCore.Extensions.JsonRpc/Attributes/JsonRpcMethodAttribute.cs +++ b/WebApiClientCore.Extensions.JsonRpc/Attributes/JsonRpcMethodAttribute.cs @@ -16,7 +16,7 @@ public class JsonRpcMethodAttribute : HttpPostAttribute, IApiFilterAttribute /// /// 获取或设置提交的Content-Type - /// 默认为application/json-rpc + /// 默认为 application/json-rpc /// public string ContentType { get; set; } = JsonRpcContent.MediaType; diff --git a/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcContent.cs b/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcContent.cs index 4014bfce..f91a6ed1 100644 --- a/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcContent.cs +++ b/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcContent.cs @@ -15,7 +15,7 @@ sealed class JsonRpcContent : BufferContent public static string MediaType => "application/json-rpc"; /// - /// uft8的json内容 + /// uft8 的 json 内容 /// /// /// 对象值 diff --git a/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcParameters.cs b/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcParameters.cs index 05ec4995..139d538b 100644 --- a/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcParameters.cs +++ b/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcParameters.cs @@ -10,7 +10,7 @@ namespace WebApiClientCore.Extensions.JsonRpc sealed class JsonRpcParameters : List { /// - /// 转换为jsonRpc请求参数 + /// 转换为 jsonRpc 请求参数 /// /// /// diff --git a/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcRequest.cs b/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcRequest.cs index 24e6563f..49373521 100644 --- a/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcRequest.cs +++ b/WebApiClientCore.Extensions.JsonRpc/Internals/JsonRpcRequest.cs @@ -15,7 +15,7 @@ sealed class JsonRpcRequest private static int @id = 0; /// - /// jsonrpc + /// json rpc /// 2.0 /// [JsonPropertyName("jsonrpc")] diff --git a/WebApiClientCore.Extensions.JsonRpc/JsonRpcResult.cs b/WebApiClientCore.Extensions.JsonRpc/JsonRpcResult.cs index 30ba0ee0..619393f8 100644 --- a/WebApiClientCore.Extensions.JsonRpc/JsonRpcResult.cs +++ b/WebApiClientCore.Extensions.JsonRpc/JsonRpcResult.cs @@ -15,7 +15,7 @@ public class JsonRpcResult public int? Id { get; set; } /// - /// jsonrpc版本号 + /// json rpc版本号 /// [JsonPropertyName("jsonrpc")] public string JsonRpc { get; set; } = string.Empty; diff --git a/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj b/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj index 83cfbd95..087443e5 100644 --- a/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj +++ b/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj @@ -3,7 +3,7 @@ enable netstandard2.1 - $(TargetPath)\$(AssemblyName).xml + True true Sign.snk diff --git a/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs index 26efd7a4..b9abff65 100644 --- a/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs +++ b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs @@ -11,7 +11,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用Json.Net序列化参数值得到的json文本作为application/json请求 + /// 使用Json.Net序列化参数值得到的 json 文本作为 application/json 请求 /// 每个Api只能注明于其中的一个参数 /// public class JsonNetContentAttribute : HttpContentAttribute @@ -32,14 +32,14 @@ public string CharSet } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// /// protected override Task SetHttpContentAsync(ApiParameterContext context) { var name = context.HttpContext.OptionsName; - var options = context.HttpContext.ServiceProvider.GetService>().Get(name); + var options = context.HttpContext.ServiceProvider.GetRequiredService>().Get(name); var json = context.ParameterValue == null ? string.Empty : JsonConvert.SerializeObject(context.ParameterValue, options.JsonSerializeOptions); diff --git a/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetReturnAttribute.cs b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetReturnAttribute.cs index df83ced1..2887da02 100644 --- a/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetReturnAttribute.cs +++ b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetReturnAttribute.cs @@ -45,7 +45,7 @@ public override async Task SetResultAsync(ApiResponseContext context) var resultType = context.ActionDescriptor.Return.DataType.Type; var name = context.HttpContext.OptionsName; - var options = context.HttpContext.ServiceProvider.GetService>().Get(name); + var options = context.HttpContext.ServiceProvider.GetRequiredService>().Get(name); context.Result = JsonConvert.DeserializeObject(json, resultType, options.JsonDeserializeOptions); } diff --git a/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj b/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj index 5577b232..e34ca33b 100644 --- a/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj +++ b/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj @@ -3,7 +3,7 @@ enable netstandard2.1 - $(TargetPath)\$(AssemblyName).xml + True true Sign.snk diff --git a/WebApiClientCore.Extensions.OAuths/Attributes/ClientCredentialsTokenAttribute.cs b/WebApiClientCore.Extensions.OAuths/Attributes/ClientCredentialsTokenAttribute.cs index 8dba09a6..f4a8d28c 100644 --- a/WebApiClientCore.Extensions.OAuths/Attributes/ClientCredentialsTokenAttribute.cs +++ b/WebApiClientCore.Extensions.OAuths/Attributes/ClientCredentialsTokenAttribute.cs @@ -3,8 +3,8 @@ namespace WebApiClientCore.Attributes { /// - /// 表示token应用特性 - /// 需要注册services.AddClientCredentialsTokenProvider + /// 表示 token 应用特性 + /// 需要注册 services.AddClientCredentialsTokenProvider /// [Obsolete("请使用OAuthTokenAttribute替换")] public class ClientCredentialsTokenAttribute : OAuthTokenAttribute diff --git a/WebApiClientCore.Extensions.OAuths/Attributes/OAuthTokenAttribute.cs b/WebApiClientCore.Extensions.OAuths/Attributes/OAuthTokenAttribute.cs index 28ef2e37..87bc385c 100644 --- a/WebApiClientCore.Extensions.OAuths/Attributes/OAuthTokenAttribute.cs +++ b/WebApiClientCore.Extensions.OAuths/Attributes/OAuthTokenAttribute.cs @@ -8,7 +8,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示token应用特性 + /// 表示 token 应用特性 /// 需要为接口或接口的基础接口注册TokenProvider /// /// @@ -18,12 +18,12 @@ namespace WebApiClientCore.Attributes public class OAuthTokenAttribute : ApiFilterAttribute { /// - /// 获取指定TokenProvider别名的方法参数名 + /// 获取指定 TokenProvider 别名的方法参数名 /// public string? AliasParameterName { get; } /// - /// 获取或设置token提供者的查找模式 + /// 获取或设置 token 提供者的查找模式 /// public TypeMatchMode TokenProviderSearchMode { get; set; } = TypeMatchMode.TypeOrBaseTypes; @@ -69,7 +69,7 @@ public sealed override Task OnResponseAsync(ApiResponseContext context) } /// - /// 获取token提供者 + /// 获取 token 提供者 /// /// 上下文 /// @@ -93,7 +93,7 @@ protected virtual ITokenProvider GetTokenProvider(ApiRequestContext context) } /// - /// 应用token + /// 应用 token /// 默认为添加到请求头的Authorization /// /// 请求上下文 @@ -107,7 +107,7 @@ protected virtual void UseTokenResult(ApiRequestContext context, TokenResult tok /// /// 返回响应是否为未授权状态 - /// 反回true则强制清除token以支持下次获取到新的token + /// 反回 true 则强制清除 token 以支持下次获取到新的 token /// /// protected virtual bool IsUnauthorized(ApiResponseContext context) diff --git a/WebApiClientCore.Extensions.OAuths/Attributes/PasswordCredentialsTokenAttribute.cs b/WebApiClientCore.Extensions.OAuths/Attributes/PasswordCredentialsTokenAttribute.cs index aea939c1..c44a00b1 100644 --- a/WebApiClientCore.Extensions.OAuths/Attributes/PasswordCredentialsTokenAttribute.cs +++ b/WebApiClientCore.Extensions.OAuths/Attributes/PasswordCredentialsTokenAttribute.cs @@ -3,8 +3,8 @@ namespace WebApiClientCore.Attributes { /// - /// 表示token应用特性 - /// 需要注册services.AddPasswordCredentialsTokenProvider + /// 表示 token 应用特性 + /// 需要注册 services.AddPasswordCredentialsTokenProvider /// [Obsolete("请使用OAuthTokenAttribute替换")] public class PasswordCredentialsTokenAttribute : OAuthTokenAttribute diff --git a/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicDependencyAttribute.cs b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicDependencyAttribute.cs new file mode 100644 index 00000000..82b6f844 --- /dev/null +++ b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicDependencyAttribute.cs @@ -0,0 +1,32 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// 表示动态依赖属性 + /// + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = true, Inherited = false)] + sealed class DynamicDependencyAttribute : Attribute + { + /// + /// 获取或设置动态访问的成员类型 + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// 获取或设置依赖的类型 + /// + public Type? Type { get; } + + /// + /// 初始化 类的新实例 + /// + /// 动态访问的成员类型 + /// 依赖的类型 + public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type) + { + this.MemberTypes = memberTypes; + this.Type = type; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 00000000..54bcd902 --- /dev/null +++ b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,85 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies the types of dynamically accessed members. + /// + enum DynamicallyAccessedMemberTypes + { + /// + /// All member types are dynamically accessed. + /// + All = -1, + + /// + /// No member types are dynamically accessed. + /// + None = 0, + + /// + /// Public parameterless constructors are dynamically accessed. + /// + PublicParameterlessConstructor = 1, + + /// + /// Public constructors are dynamically accessed. + /// + PublicConstructors = 3, + + /// + /// Non-public constructors are dynamically accessed. + /// + NonPublicConstructors = 4, + + /// + /// Public methods are dynamically accessed. + /// + PublicMethods = 8, + + /// + /// Non-public methods are dynamically accessed. + /// + NonPublicMethods = 16, + + /// + /// Public fields are dynamically accessed. + /// + PublicFields = 32, + + /// + /// Non-public fields are dynamically accessed. + /// + NonPublicFields = 64, + + /// + /// Public nested types are dynamically accessed. + /// + PublicNestedTypes = 128, + + /// + /// Non-public nested types are dynamically accessed. + /// + NonPublicNestedTypes = 256, + + /// + /// Public properties are dynamically accessed. + /// + PublicProperties = 512, + + /// + /// Non-public properties are dynamically accessed. + /// + NonPublicProperties = 1024, + + /// + /// Public events are dynamically accessed. + /// + PublicEvents = 2048, + + /// + /// Non-public events are dynamically accessed. + /// + NonPublicEvents = 4096, + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs new file mode 100644 index 00000000..30a744b9 --- /dev/null +++ b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the members accessed dynamically at runtime are considered used. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false)] + sealed class DynamicallyAccessedMembersAttribute : Attribute + { + /// + /// Gets the types of dynamically accessed members. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// Initializes a new instance of the class with the specified member types. + /// + /// The types of dynamically accessed members. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + this.MemberTypes = memberTypes; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresDynamicCodeAttribute.cs b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresDynamicCodeAttribute.cs new file mode 100644 index 00000000..e94faa6d --- /dev/null +++ b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresDynamicCodeAttribute.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_1 || NET5_0 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the attributed class, constructor, or method requires dynamic code. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + sealed class RequiresDynamicCodeAttribute : Attribute + { + /// + /// Gets the message associated with the requirement for dynamic code. + /// + public string Message { get; } + + /// + /// Initializes a new instance of the class with the specified message. + /// + /// The message associated with the requirement for dynamic code. + public RequiresDynamicCodeAttribute(string message) + { + this.Message = message; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 00000000..ee93f9d3 --- /dev/null +++ b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,22 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// 获取或设置对于未引用代码的要求的消息。 + /// + public string Message { get; } + + /// + /// 初始化 类的新实例。 + /// + /// 对于未引用代码的要求的消息。 + public RequiresUnreferencedCodeAttribute(string message) + { + this.Message = message; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Extensions.OAuths/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs new file mode 100644 index 00000000..caa21e39 --- /dev/null +++ b/WebApiClientCore.Extensions.OAuths/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs @@ -0,0 +1,52 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// 表示一个用于取消对代码分析器规则的警告的特性 + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + sealed class UnconditionalSuppressMessageAttribute : Attribute + { + /// + /// 获取或设置警告的类别 + /// + public string Category { get; } + + /// + /// 获取或设置要取消的检查标识符 + /// + public string CheckId { get; } + + /// + /// 获取或设置取消警告的理由 + /// + public string? Justification { get; set; } + + /// + /// 获取或设置消息的标识符 + /// + public string? MessageId { get; set; } + + /// + /// 获取或设置取消警告的范围 + /// + public string? Scope { get; set; } + + /// + /// 获取或设置取消警告的目标 + /// + public string? Target { get; set; } + + /// + /// 初始化 类的新实例 + /// + /// 警告的类别 + /// 要取消的检查标识符 + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + this.Category = category; + this.CheckId = checkId; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/ITokenProviderBuilder.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/ITokenProviderBuilder.cs index 886bbc9d..e6c14583 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/ITokenProviderBuilder.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/ITokenProviderBuilder.cs @@ -6,7 +6,7 @@ public interface ITokenProviderBuilder { /// - /// 获取token提供者的名称 + /// 获取 token 提供者的名称 /// string Name { get; } diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs index 4a2838bd..65788b57 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs @@ -1,16 +1,17 @@ using System; +using System.Diagnostics.CodeAnalysis; using WebApiClientCore.Extensions.OAuths; using WebApiClientCore.Extensions.OAuths.HttpMessageHandlers; namespace Microsoft.Extensions.DependencyInjection { /// - /// 提供OAuth授权token应用的http消息处理程序扩展 + /// 提供OAuth授权 token 应用的 http 消息处理程序扩展 /// public static class TokenHandlerExtensions { /// - /// 添加token应用的http消息处理程序 + /// 添加 token 应用的 http 消息处理程序 /// 需要为接口或接口的基础接口注册TokenProvider /// /// @@ -26,7 +27,7 @@ public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder bu } /// - /// 添加token应用的http消息处理程序 + /// 添加 token 应用的 http 消息处理程序 /// 需要为接口或接口的基础接口注册TokenProvider /// /// @@ -35,16 +36,17 @@ public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder bu /// /// /// - /// hanlder的创建委托 + /// handler的创建委托 /// token提供者的查找模式 /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "类型 httpApiType 明确是不会被裁剪的")] public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder builder, Func handlerFactory, TypeMatchMode tokenProviderSearchMode = TypeMatchMode.TypeOrBaseTypes) where TOAuthTokenHandler : OAuthTokenHandler { var httpApiType = builder.GetHttpApiType(); if (httpApiType == null) { - throw new InvalidOperationException($"无效的{nameof(IHttpClientBuilder)},找不到其关联的http接口类型"); + throw new InvalidOperationException($"无效的{nameof(IHttpClientBuilder)},找不到其关联的 http 接口类型"); } return builder.AddHttpMessageHandler(serviceProvider => diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs index 1cfaa0e4..1489867c 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Options; using System; +using System.Diagnostics.CodeAnalysis; using WebApiClientCore.Extensions.OAuths.TokenProviders; namespace Microsoft.Extensions.DependencyInjection @@ -10,39 +11,46 @@ namespace Microsoft.Extensions.DependencyInjection public static partial class TokenProviderExtensions { /// - /// 为指定接口添加Client模式的token提供者 + /// 为指定接口添加Client模式的 token 提供者 /// /// 接口类型 /// /// TokenProvider的别名 /// - public static OptionsBuilder AddClientCredentialsTokenProvider(this IServiceCollection services, string alias = "") - { + public static OptionsBuilder AddClientCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + string alias = "") + { var builder = services.AddTokenProvider(alias); return new OptionsBuilder(builder.Services, builder.Name); } /// - /// 为指定接口添加Client模式的token提供者 + /// 为指定接口添加Client模式的 token 提供者 /// /// 接口类型 /// /// 配置 /// - public static OptionsBuilder AddClientCredentialsTokenProvider(this IServiceCollection services, Action configureOptions) + public static OptionsBuilder AddClientCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Action configureOptions) { return services.AddClientCredentialsTokenProvider().Configure(configureOptions); } /// - /// 为指定接口添加Client模式的token提供者 + /// 为指定接口添加Client模式的 token 提供者 /// /// 接口类型 /// /// TokenProvider的别名 /// 配置 /// - public static OptionsBuilder AddClientCredentialsTokenProvider(this IServiceCollection services, string alias, Action configureOptions) + public static OptionsBuilder AddClientCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + string alias, + Action configureOptions) { return services.AddClientCredentialsTokenProvider(alias).Configure(configureOptions); } diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs index 3e567e18..2c658e36 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Options; using System; +using System.Diagnostics.CodeAnalysis; using WebApiClientCore.Extensions.OAuths.TokenProviders; namespace Microsoft.Extensions.DependencyInjection @@ -10,39 +11,46 @@ namespace Microsoft.Extensions.DependencyInjection public static partial class TokenProviderExtensions { /// - /// 为指定接口添加Password模式的token提供者 + /// 为指定接口添加Password模式的 token 提供者 /// /// 接口类型 /// /// TokenProvider的别名 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider(this IServiceCollection services, string alias = "") + public static OptionsBuilder AddPasswordCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + string alias = "") { var builder = services.AddTokenProvider(alias); return new OptionsBuilder(builder.Services, builder.Name); } /// - /// 为指定接口添加Password模式的token提供者 + /// 为指定接口添加Password模式的 token 提供者 /// /// 接口类型 /// /// 配置 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider(this IServiceCollection services, Action configureOptions) + public static OptionsBuilder AddPasswordCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Action configureOptions) { return services.AddPasswordCredentialsTokenProvider().Configure(configureOptions); } /// - /// 为指定接口添加Password模式的token提供者 + /// 为指定接口添加Password模式的 token 提供者 /// /// 接口类型 /// /// TokenProvider的别名 /// 配置 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider(this IServiceCollection services, string alias, Action configureOptions) + public static OptionsBuilder AddPasswordCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + string alias, + Action configureOptions) { return services.AddPasswordCredentialsTokenProvider(alias).Configure(configureOptions); } diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs index d8770691..a486023d 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore; using WebApiClientCore.Extensions.OAuths; @@ -13,20 +14,23 @@ namespace Microsoft.Extensions.DependencyInjection public static partial class TokenProviderExtensions { /// - /// 为指定接口添加token提供者 + /// 为指定接口添加 token 提供者 /// /// 接口类型 /// /// token请求委托 /// TokenProvider的别名 /// - public static ITokenProviderBuilder AddTokenProvider(this IServiceCollection services, Func> tokenRequest, string alias = "") + public static ITokenProviderBuilder AddTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Func> tokenRequest, + string alias = "") { return services.AddTokenProvider(s => new DelegateTokenProvider(s, tokenRequest), alias); } /// - /// 为指定接口添加token提供者 + /// 为指定接口添加 token 提供者 /// /// 接口类型 /// token提供者类型 @@ -35,15 +39,11 @@ public static ITokenProviderBuilder AddTokenProvider(this IServiceColl /// TokenProvider的别名 /// public static ITokenProviderBuilder AddTokenProvider< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi, -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - TTokenProvider>(this IServiceCollection services, Func tokenProviderFactory, string alias = "") - where TTokenProvider : class, ITokenProvider + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTokenProvider>( + this IServiceCollection services, + Func tokenProviderFactory, + string alias = "") where TTokenProvider : class, ITokenProvider { return services .RemoveAll() @@ -53,7 +53,7 @@ public static ITokenProviderBuilder AddTokenProvider< /// - /// 为指定接口添加token提供者 + /// 为指定接口添加 token 提供者 /// /// 接口类型 /// token提供者类型 @@ -61,15 +61,10 @@ public static ITokenProviderBuilder AddTokenProvider< /// TokenProvider的别名 /// public static ITokenProviderBuilder AddTokenProvider< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi, -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - TTokenProvider>(this IServiceCollection services, string alias = "") - where TTokenProvider : class, ITokenProvider + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTokenProvider>( + this IServiceCollection services, + string alias = "") where TTokenProvider : class, ITokenProvider { return services .RemoveAll() @@ -79,15 +74,18 @@ public static ITokenProviderBuilder AddTokenProvider< /// - /// 向token工厂提供者添加token提供者 + /// 向 token 工厂提供者添加 token 提供者 /// /// 接口类型 /// token提供者类型 /// /// TokenProvider的别名 /// - private static ITokenProviderBuilder AddTokenProviderCore(this IServiceCollection services, string alias) - where TTokenProvider : class, ITokenProvider + private static TokenProviderBuilder AddTokenProviderCore< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTokenProvider>( + this IServiceCollection services, + string alias) where TTokenProvider : class, ITokenProvider { if (alias == null) { @@ -117,7 +115,7 @@ private static ITokenProviderBuilder AddTokenProviderCore - /// 获取token提供者的名称 + /// 获取 token 提供者的名称 /// public string Name { get; } diff --git a/WebApiClientCore.Extensions.OAuths/Exceptions/TokenEndPointNullException.cs b/WebApiClientCore.Extensions.OAuths/Exceptions/TokenEndPointNullException.cs index a18cbefa..1343e1d0 100644 --- a/WebApiClientCore.Extensions.OAuths/Exceptions/TokenEndPointNullException.cs +++ b/WebApiClientCore.Extensions.OAuths/Exceptions/TokenEndPointNullException.cs @@ -1,12 +1,12 @@ namespace WebApiClientCore.Extensions.OAuths.Exceptions { /// - /// 表示获取Toke的Url节点为nul的异常 + /// 表示获取Toke的Url节点为 null 的异常 /// public class TokenEndPointNullException : TokenException { /// - /// 获取Toke的Url节点为nul的异常 + /// 获取Toke的Url节点为 null 的异常 /// public TokenEndPointNullException() : base("The Endpoint is required") diff --git a/WebApiClientCore.Extensions.OAuths/Exceptions/TokenNullException.cs b/WebApiClientCore.Extensions.OAuths/Exceptions/TokenNullException.cs index 3f7230fb..0638f973 100644 --- a/WebApiClientCore.Extensions.OAuths/Exceptions/TokenNullException.cs +++ b/WebApiClientCore.Extensions.OAuths/Exceptions/TokenNullException.cs @@ -1,12 +1,12 @@ namespace WebApiClientCore.Extensions.OAuths.Exceptions { /// - /// 表示空token异常 + /// 表示空 token 异常 /// public class TokenNullException : TokenException { /// - /// 空token异常 + /// 空 token 异常 /// public TokenNullException() : base("Unable to get token") diff --git a/WebApiClientCore.Extensions.OAuths/HttpMessageHandlers/OAuthTokenHandler.cs b/WebApiClientCore.Extensions.OAuths/HttpMessageHandlers/OAuthTokenHandler.cs index 62024d84..f66b5b7a 100644 --- a/WebApiClientCore.Extensions.OAuths/HttpMessageHandlers/OAuthTokenHandler.cs +++ b/WebApiClientCore.Extensions.OAuths/HttpMessageHandlers/OAuthTokenHandler.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Extensions.OAuths.HttpMessageHandlers { /// - /// 表示token应用的http消息处理程序 + /// 表示 token 应用的 http 消息处理程序 /// public class OAuthTokenHandler : AuthorizationHandler { @@ -17,7 +17,7 @@ public class OAuthTokenHandler : AuthorizationHandler private readonly ITokenProvider tokenProvider; /// - /// token应用的http消息处理程序 + /// token 应用的 http 消息处理程序 /// /// token提供者 public OAuthTokenHandler(ITokenProvider tokenProvider) @@ -44,7 +44,7 @@ protected sealed override async Task SetAuthorizationAsync(SetReason reason, Htt } /// - /// 应用token + /// 应用 token /// 默认为添加到请求头的Authorization /// /// 请求上下文 diff --git a/WebApiClientCore.Extensions.OAuths/ITokenProvider.cs b/WebApiClientCore.Extensions.OAuths/ITokenProvider.cs index 3b16359f..09a2ff87 100644 --- a/WebApiClientCore.Extensions.OAuths/ITokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/ITokenProvider.cs @@ -3,7 +3,7 @@ namespace WebApiClientCore.Extensions.OAuths { /// - /// 定义token提供者的接口 + /// 定义 token 提供者的接口 /// public interface ITokenProvider { @@ -13,12 +13,12 @@ public interface ITokenProvider string Name { set; } /// - /// 强制清除token以支持下次获取到新的token + /// 强制清除 token 以支持下次获取到新的 token /// void ClearToken(); /// - /// 获取token信息 + /// 获取 token 信息 /// /// Task GetTokenAsync(); diff --git a/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs b/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs index 27e7b9fd..28b60e96 100644 --- a/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace WebApiClientCore.Extensions.OAuths { @@ -8,22 +9,27 @@ namespace WebApiClientCore.Extensions.OAuths public interface ITokenProviderFactory { /// - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 /// /// - ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); + ITokenProvider Create( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); /// - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 /// TokenProvider的别名 /// /// - ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode, string alias); + ITokenProvider Create( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode, + string alias); } } diff --git a/WebApiClientCore.Extensions.OAuths/ITokenProviderService.cs b/WebApiClientCore.Extensions.OAuths/ITokenProviderService.cs index 1a5b65da..efe39155 100644 --- a/WebApiClientCore.Extensions.OAuths/ITokenProviderService.cs +++ b/WebApiClientCore.Extensions.OAuths/ITokenProviderService.cs @@ -1,12 +1,12 @@ namespace WebApiClientCore.Extensions.OAuths { /// - /// 定义http接口的token提供者服务 + /// 定义 http 接口的 token 提供者服务 /// interface ITokenProviderService { /// - /// 获取token提供者 + /// 获取 token 提供者 /// ITokenProvider TokenProvider { get; } diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs index 9a23b1c6..402b8005 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs @@ -2,11 +2,12 @@ using Microsoft.Extensions.Options; using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; namespace WebApiClientCore.Extensions.OAuths { /// - /// 表示默认的token提供者工厂 + /// 表示默认的 token 提供者工厂 /// sealed class TokenProviderFactory : ITokenProviderFactory { @@ -15,7 +16,7 @@ sealed class TokenProviderFactory : ITokenProviderFactory private readonly ConcurrentDictionary tokenProviderCache = new(); /// - /// 默认的token提供者工厂 + /// 默认的 token 提供者工厂 /// /// /// @@ -26,27 +27,32 @@ public TokenProviderFactory(IServiceProvider serviceProvider, IOptions - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 /// /// /// - public ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode) + public ITokenProvider Create( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly) { return this.Create(httpApiType, typeMatchMode, alias: string.Empty); } /// - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 /// TokenProvider的别名 /// /// - public ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode, string alias) + public ITokenProvider Create( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode, + string alias) { if (httpApiType == null) { @@ -62,7 +68,7 @@ public ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode, stri } /// - /// 创建其对应的token提供者 + /// 创建其对应的 token 提供者 /// /// 缓存的键 /// @@ -89,8 +95,8 @@ private ITokenProvider CreateTokenProvider(ServiceKey serviceKey) var message = string.IsNullOrEmpty(alias) - ? $"尚未注册{httpApiType}无别名的token提供者" - : $"尚未注册{httpApiType}别名为{alias}的token提供者"; + ? $"尚未注册 {httpApiType} 无别名的 token 提供者" + : $"尚未注册 {httpApiType} 别名为{alias}的 token 提供者"; throw new InvalidOperationException(message); } @@ -101,7 +107,9 @@ private ITokenProvider CreateTokenProvider(ServiceKey serviceKey) /// 别名 /// /// - private ITokenProvider? CreateTokenProviderFromBaseType(Type httpApiType, string alias) + private ITokenProvider? CreateTokenProviderFromBaseType( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + string alias) { foreach (var baseType in httpApiType.GetInterfaces()) { @@ -122,13 +130,17 @@ private sealed class ServiceKey : IEquatable { private int? hashCode; + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type HttpApiType { get; } public TypeMatchMode TypeMatchMode { get; } public string Alias { get; } - public ServiceKey(Type httpApiType, TypeMatchMode typeMatchMode, string alias) + public ServiceKey( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode, + string alias) { this.HttpApiType = httpApiType; this.TypeMatchMode = typeMatchMode; diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviderFactoryOptions.cs b/WebApiClientCore.Extensions.OAuths/TokenProviderFactoryOptions.cs index 5c1c2580..31a143bd 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviderFactoryOptions.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviderFactoryOptions.cs @@ -18,12 +18,12 @@ sealed class TokenProviderFactoryOptions /// 登记映射 /// /// 接口类型 - /// 提供者类型 + /// 提供者类型 /// TokenProvider的别名 - public void Register(string alias) where TTokenPrivder : ITokenProvider + public void Register(string alias) where TTokenProvider : ITokenProvider { var httpApiType = typeof(THttpApi); - var serviceType = typeof(TokenProviderService); + var serviceType = typeof(TokenProviderService); if (this.httpApiServiceDescriptors.TryGetValue(httpApiType, out var existDescriptor) && existDescriptor.ServiceType == serviceType) diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviderService.cs b/WebApiClientCore.Extensions.OAuths/TokenProviderService.cs index cf39f319..029126f3 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviderService.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviderService.cs @@ -1,21 +1,21 @@ namespace WebApiClientCore.Extensions.OAuths { /// - /// 表示http接口的token提供者服务 + /// 表示 http 接口的 token 提供者服务 /// /// /// sealed class TokenProviderService : ITokenProviderService where TTokenProvider : ITokenProvider { /// - /// 获取token提供者 + /// 获取 token 提供者 /// public ITokenProvider TokenProvider { get; } /// - /// http接口的token提供者服务 + /// http 接口的 token 提供者服务 /// - /// token提供者 + /// token 提供者 public TokenProviderService(TTokenProvider tokenProvider) { this.TokenProvider = tokenProvider; diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsOptions.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsOptions.cs index 75d11558..516f5cfb 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsOptions.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsOptions.cs @@ -10,15 +10,15 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders public class ClientCredentialsOptions { /// - /// 获取或设置提供Token获取的Url节点 + /// 获取或设置提供 token 获取的Url节点 /// [Required] [DisallowNull] public Uri? Endpoint { get; set; } /// - /// 是否尝试使用token刷新功能 - /// 禁用则token过期时总是去请求新token + /// 是否尝试使用 token 刷新功能 + /// 禁用则 token 过期时总是去请求新 token /// public bool UseRefreshToken { get; set; } = true; diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs index 7bb19a34..83f122a5 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs @@ -1,17 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore.Extensions.OAuths.Exceptions; namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示Client模式的token提供者 + /// 表示Client模式的 token 提供者 /// public class ClientCredentialsTokenProvider : TokenProvider { /// - /// Client模式的token提供者 + /// Client模式的 token 提供者 /// /// public ClientCredentialsTokenProvider(IServiceProvider services) @@ -20,10 +21,12 @@ public ClientCredentialsTokenProvider(IServiceProvider services) } /// - /// 请求获取token + /// 请求获取 token /// /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("Trimming", "IL3050:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] protected override Task RequestTokenAsync(IServiceProvider serviceProvider) { var options = this.GetOptionsValue(); @@ -37,11 +40,13 @@ public ClientCredentialsTokenProvider(IServiceProvider services) } /// - /// 刷新token + /// 刷新 token /// /// /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("Trimming", "IL3050:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) { var options = this.GetOptionsValue(); diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/DelegateTokenProvider.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/DelegateTokenProvider.cs index fe75bbc8..64ad4159 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/DelegateTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/DelegateTokenProvider.cs @@ -4,7 +4,7 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示指定委托请求Token提供者 + /// 表示指定委托请求 token 提供者 /// sealed class DelegateTokenProvider : TokenProvider { @@ -14,7 +14,7 @@ sealed class DelegateTokenProvider : TokenProvider private readonly Func> tokenRequest; /// - /// 指定委托请求Token提供者 + /// 指定委托请求 token 提供者 /// /// /// token请求委托 @@ -25,7 +25,7 @@ public DelegateTokenProvider(IServiceProvider services, Func - /// 请求获取token + /// 请求获取 token /// /// 服务提供者 /// @@ -35,10 +35,10 @@ public DelegateTokenProvider(IServiceProvider services, Func - /// 刷新token + /// 刷新 token /// /// 服务提供者 - /// 刷新token + /// 刷新 token /// protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) { diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs index 327e6561..b0a81167 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.Options; using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; -using System.Text.Json; +using System.Net.Http.Json; using System.Threading.Tasks; using WebApiClientCore.Attributes; using WebApiClientCore.HttpContents; @@ -30,39 +31,45 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor - /// 以client_credentials授权方式获取token + /// 以 client_credentials 授权方式获取 token /// /// token请求地址 /// 身份信息 /// [HttpPost] [FormField("grant_type", "client_credentials")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public Task RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] ClientCredentials credentials) { return this.PostFormAsync(endpoint, "client_credentials", credentials); } /// - /// 以password授权方式获取token + /// 以 password 授权方式获取 token /// /// token请求地址 /// 身份信息 /// [HttpPost] [FormField("grant_type", "password")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public Task RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] PasswordCredentials credentials) { return this.PostFormAsync(endpoint, "password", credentials); } /// - /// 刷新token + /// 刷新 token /// /// token请求地址 /// 身份信息 /// [HttpPost] [FormField("grant_type", "refresh_token")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public Task RefreshTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] RefreshTokenCredentials credentials) { return this.PostFormAsync(endpoint, "refresh_token", credentials); @@ -76,18 +83,15 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor /// /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] private async Task PostFormAsync(Uri endpoint, string grant_type, TCredentials credentials) { using var formContent = new FormContent(credentials, this.httpApiOptions.KeyValueSerializeOptions); formContent.AddFormField(new KeyValue("grant_type", grant_type)); var response = await this.httpClientFactory.CreateClient().PostAsync(endpoint, formContent); - var utf8Json = await response.Content.ReadAsUtf8ByteArrayAsync(); - if (utf8Json.Length == 0) - { - return default; - } - return JsonSerializer.Deserialize(utf8Json, this.httpApiOptions.JsonDeserializeOptions); + return await response.Content.ReadFromJsonAsync(this.httpApiOptions.JsonDeserializeOptions); } } } diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsOptions.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsOptions.cs index ce80c5a5..1e5ea49f 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsOptions.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsOptions.cs @@ -10,15 +10,15 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders public class PasswordCredentialsOptions { /// - /// 获取或设置提供Token获取的Url节点 + /// 获取或设置提供 token 获取的Url节点 /// [Required] [DisallowNull] public Uri? Endpoint { get; set; } /// - /// 是否尝试使用token刷新功能 - /// 禁用则token过期时总是去请求新token + /// 是否尝试使用 token 刷新功能 + /// 禁用则 token 过期时总是去请求新 token /// public bool UseRefreshToken { get; set; } = true; diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs index 7e7c9ef8..ff188a93 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs @@ -1,17 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore.Extensions.OAuths.Exceptions; namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示Password模式的token提供者 + /// 表示 Password 模式的 token 提供者 /// public class PasswordCredentialsTokenProvider : TokenProvider { /// - /// Password模式的token提供者 + /// Password 模式的 token 提供者 /// /// public PasswordCredentialsTokenProvider(IServiceProvider services) @@ -20,10 +21,12 @@ public PasswordCredentialsTokenProvider(IServiceProvider services) } /// - /// 请求获取token + /// 请求获取 token /// /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("Trimming", "IL3050:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] protected override Task RequestTokenAsync(IServiceProvider serviceProvider) { var options = this.GetOptionsValue(); @@ -37,11 +40,13 @@ public PasswordCredentialsTokenProvider(IServiceProvider services) } /// - /// 刷新token + /// 刷新 token /// /// /// - /// + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("Trimming", "IL3050:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) { var options = this.GetOptionsValue(); diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/RefreshTokenCredentials.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/RefreshTokenCredentials.cs index 8e030541..25c105a1 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/RefreshTokenCredentials.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/RefreshTokenCredentials.cs @@ -1,12 +1,12 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示用于刷新token的身份信息 + /// 表示用于刷新 token 的身份信息 /// public class RefreshTokenCredentials : Credentials { /// - /// 刷新token值 + /// 刷新 token值 /// public string? Refresh_token { get; set; } diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/TokenProvider.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/TokenProvider.cs index dd77ad75..1ea0ec94 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/TokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/TokenProvider.cs @@ -7,12 +7,12 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示Token提供者抽象类 + /// 表示 token 提供者抽象类 /// public abstract class TokenProvider : ITokenProvider { /// - /// 最近请求到的token + /// 最近请求到的 token /// private TokenResult? token; @@ -32,7 +32,7 @@ public abstract class TokenProvider : ITokenProvider public string Name { get; set; } = string.Empty; /// - /// Token提供者抽象类 + /// token 提供者抽象类 /// /// public TokenProvider(IServiceProvider services) @@ -42,7 +42,7 @@ public TokenProvider(IServiceProvider services) /// /// 获取选项值 - /// Options名称为本类型的Name属性 + /// Options 名称为本类型的 Name 属性 /// /// /// @@ -52,7 +52,7 @@ public TOptions GetOptionsValue() } /// - /// 强制清除token以支持下次获取到新的token + /// 强制清除 token 以支持下次获取到新的 token /// public void ClearToken() { @@ -63,7 +63,7 @@ public void ClearToken() } /// - /// 获取token信息 + /// 获取 token 信息 /// /// public async Task GetTokenAsync() @@ -84,32 +84,28 @@ public async Task GetTokenAsync() : await this.RefreshTokenAsync(scope.ServiceProvider, this.token.Refresh_token ?? string.Empty).ConfigureAwait(false); } - if (this.token == null) - { - throw new TokenNullException(); - } - return this.token.EnsureSuccess(); + return this.token == null ? throw new TokenNullException() : this.token.EnsureSuccess(); } } /// - /// 请求获取token + /// 请求获取 token /// /// 服务提供者 /// protected abstract Task RequestTokenAsync(IServiceProvider serviceProvider); /// - /// 刷新token + /// 刷新 token /// /// 服务提供者 - /// 刷新token + /// 刷新 token /// protected abstract Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token); /// - /// 转换为string + /// 转换为 string /// /// public override string ToString() diff --git a/WebApiClientCore.Extensions.OAuths/TokenResult.cs b/WebApiClientCore.Extensions.OAuths/TokenResult.cs index ffcbe916..71f312e3 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenResult.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenResult.cs @@ -52,16 +52,12 @@ public class TokenResult public string? Error { get; set; } /// - /// 确保token成功 + /// 确保 token 成功 /// /// public TokenResult EnsureSuccess() { - if (this.IsSuccess() == true) - { - return this; - } - throw new TokenException(this.Error); + return this.IsSuccess() ? this : throw new TokenException(this.Error); } /// @@ -83,7 +79,7 @@ public virtual bool IsExpired() } /// - /// 返回token是否支持刷新 + /// 返回 token 是否支持刷新 /// /// public virtual bool CanRefresh() diff --git a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj index def5a803..d9905876 100644 --- a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj +++ b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj @@ -2,8 +2,9 @@ enable - netstandard2.1;net5.0 - $(TargetPath)\$(AssemblyName).xml + True + netstandard2.1;net5.0;net8.0 + true true Sign.snk diff --git a/WebApiClientCore.Extensions.SourceGenerator/WebApiClientBuilderExtensions.cs b/WebApiClientCore.Extensions.SourceGenerator/WebApiClientBuilderExtensions.cs deleted file mode 100644 index 43f77329..00000000 --- a/WebApiClientCore.Extensions.SourceGenerator/WebApiClientBuilderExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// IWebApiClientBuilder扩展 - /// - public static class WebApiClientBuilderExtensions - { - /// - /// 编译时使用SourceGenerator生成接口的代理类型代码 - /// 运行时查找接口的代理类型并创建实例 - /// - /// - /// - [Obsolete("SourceGenerator功能已合并到基础包并默认启用")] - public static IWebApiClientBuilder UseSourceGeneratorHttpApiActivator(this IWebApiClientBuilder builder) - { - return builder; - } - } -} diff --git a/WebApiClientCore.Extensions.SourceGenerator/WebApiClientCore.Extensions.SourceGenerator.csproj b/WebApiClientCore.Extensions.SourceGenerator/WebApiClientCore.Extensions.SourceGenerator.csproj index aa29ba6b..c2f35178 100644 --- a/WebApiClientCore.Extensions.SourceGenerator/WebApiClientCore.Extensions.SourceGenerator.csproj +++ b/WebApiClientCore.Extensions.SourceGenerator/WebApiClientCore.Extensions.SourceGenerator.csproj @@ -1,18 +1,13 @@ enable - - 2.0.8 + false netstandard2.1 - $(TargetPath)\$(AssemblyName).xml - true Sign.snk - - WebApiClientCore的接口代理类代码生成扩展 - WebApiClientCore的接口代理类代码生成扩展 + 此扩展包的实现已合并到WebApiClientCore包,无任何功能 + 此扩展包的实现已合并到WebApiClientCore包,无任何功能 - diff --git a/WebApiClientCore.OpenApi.SourceGenerator/CSharpHtml.cs b/WebApiClientCore.OpenApi.SourceGenerator/CSharpHtml.cs index ad27d81c..56b1a0e8 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/CSharpHtml.cs +++ b/WebApiClientCore.OpenApi.SourceGenerator/CSharpHtml.cs @@ -116,8 +116,12 @@ public string RenderText(T model) { var html = this.RenderHtml(model); var doc = XDocument.Parse(html).Root; - var builder = new StringBuilder(); + if (doc == null) + { + return string.Empty; + } + var builder = new StringBuilder(); RenderText(doc, builder); return builder.ToString(); } @@ -157,7 +161,7 @@ private void RenderText(XElement element, StringBuilder builder) builder.Append(text); if (element.NextNode != null) { - builder.Append(" "); + builder.Append(' '); } } } diff --git a/WebApiClientCore.OpenApi.SourceGenerator/HttpApiSettings.cs b/WebApiClientCore.OpenApi.SourceGenerator/HttpApiSettings.cs index 1579d42a..bdf5a05d 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/HttpApiSettings.cs +++ b/WebApiClientCore.OpenApi.SourceGenerator/HttpApiSettings.cs @@ -29,7 +29,7 @@ public HttpApiSettings() this.ResponseArrayType = "List"; this.ResponseDictionaryType = "Dictionary"; this.ParameterArrayType = "IEnumerable"; - this.ParameterDictionaryType = "IDictionary"; + this.ParameterDictionaryType = "IDictionary"; this.OperationNameGenerator = new OperationNameProvider(); this.ParameterNameGenerator = new ParameterNameProvider(); @@ -157,8 +157,8 @@ private static string PrettyName(string name) name = name.Replace("[]", "Array"); } - var matchs = Regex.Matches(name, @"\W"); - if (matchs.Count == 0 || matchs.Count % 2 > 0) + var matches = Regex.Matches(name, @"\W"); + if (matches.Count == 0 || matches.Count % 2 > 0) { return name; } @@ -167,7 +167,7 @@ private static string PrettyName(string name) return Regex.Replace(name, @"\W", m => { index++; - return index < matchs.Count / 2 ? "Of" : null; + return index < matches.Count / 2 ? "Of" : string.Empty; }); } } diff --git a/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpApi.cshtml b/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpApi.cshtml index 9dbbc459..b2605f46 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpApi.cshtml +++ b/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpApi.cshtml @@ -1,5 +1,6 @@ @inherits HtmlTempate @using NSwag; +@using System.Security; @using WebApiClientCore.OpenApi.SourceGenerator; @@ -26,8 +27,8 @@ {
/// <summary>
foreach (var line in Model.Summary.Split(new[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries)) - { -
/// @line
+ { +
/// @SecurityElement.Escape(line)
}
/// </summary>
} @@ -48,18 +49,19 @@ {
/// <summary>
foreach (var line in method.Summary.Split(new[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries)) - { -
/// @line
+ { +
/// @SecurityElement.Escape(line)
}
/// </summary>
foreach (var parameter in method.Parameters) { var description = parameter.HasDescription ? parameter.Description.Replace("\r", "\t").Replace("\n", "\t") : null; -
/// <param name="@(parameter.VariableName)">@(description)</param>
+ +
/// <param name="@(parameter.VariableName)">@(SecurityElement.Escape(description))</param>
}
/// <param name="cancellationToken">cancellationToken</param>
-
/// <returns>@(method.ResultDescription)</returns>
+
/// <returns>@(SecurityElement.Escape(method.ResultDescription))</returns>
}
@@ -91,7 +93,7 @@ { var schema = parameter.Schema as OpenApiParameter; if (schema != null && schema.CollectionFormat != OpenApiParameterCollectionFormat.Undefined - && schema.CollectionFormat != OpenApiParameterCollectionFormat.Multi) + && schema.CollectionFormat != OpenApiParameterCollectionFormat.Multi) { [PathQuery(CollectionFormat = CollectionFormat.@(schema.CollectionFormat))] } diff --git a/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpModel.cshtml b/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpModel.cshtml index 6b4002c9..0dd18780 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpModel.cshtml +++ b/WebApiClientCore.OpenApi.SourceGenerator/Views/HttpModel.cshtml @@ -1,4 +1,5 @@ @inherits HtmlTempate +@using System.Security; @using WebApiClientCore.OpenApi.SourceGenerator; @@ -17,7 +18,7 @@ @foreach (var line in Model.Lines) { -
@line
+
@SecurityElement.Escape(line)
}
}
diff --git a/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj b/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj index 057fd4c2..e4f264f2 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj +++ b/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj @@ -24,16 +24,11 @@ PreserveNewest - + PreserveNewest $(IncludeRazorContentInPack) - PreserveNewest - - - PreserveNewest - $(IncludeRazorContentInPack) - PreserveNewest - -
+ Always + + diff --git a/WebApiClientCore.Test/Attributes/ActionAttributes/HttpHostAttributeTest.cs b/WebApiClientCore.Test/Attributes/ActionAttributes/HttpHostAttributeTest.cs index 4ed5e6e8..6bdb032f 100644 --- a/WebApiClientCore.Test/Attributes/ActionAttributes/HttpHostAttributeTest.cs +++ b/WebApiClientCore.Test/Attributes/ActionAttributes/HttpHostAttributeTest.cs @@ -15,7 +15,7 @@ public async Task OnRequestAsyncTest() var context = new TestRequestContext(apiAction, string.Empty); Assert.Throws(() => new HttpHostAttribute(null!)); - Assert.Throws(() => new HttpHostAttribute("/")); + // Assert.Throws(() => new HttpHostAttribute("/")); context.HttpContext.RequestMessage.RequestUri = null; var attr = new HttpHostAttribute("http://www.webapiclient.com"); diff --git a/WebApiClientCore.Test/Attributes/FormDataTextAttributeTest.cs b/WebApiClientCore.Test/Attributes/FormDataTextAttributeTest.cs index fb9e29b1..23597516 100644 --- a/WebApiClientCore.Test/Attributes/FormDataTextAttributeTest.cs +++ b/WebApiClientCore.Test/Attributes/FormDataTextAttributeTest.cs @@ -13,9 +13,7 @@ public class FormDataTextAttributeTest { private string get(string name, string value) { - return $@"Content-Disposition: form-data; name=""{name}"" - -{HttpUtility.UrlEncode(value, Encoding.UTF8)}"; + return $@"Content-Disposition: form-data; name=""{name}""{"\r\n\r\n"}{HttpUtility.UrlEncode(value, Encoding.UTF8)}"; } [Fact] diff --git a/WebApiClientCore.Test/Attributes/ParameterAttributes/JsonContentAttributeTest.cs b/WebApiClientCore.Test/Attributes/ParameterAttributes/JsonContentAttributeTest.cs index 828aeda3..c7ba54a0 100644 --- a/WebApiClientCore.Test/Attributes/ParameterAttributes/JsonContentAttributeTest.cs +++ b/WebApiClientCore.Test/Attributes/ParameterAttributes/JsonContentAttributeTest.cs @@ -13,7 +13,7 @@ namespace WebApiClientCore.Test.Attributes.ParameterAttributes public class JsonContentAttributeTest { [Fact] - public async Task BeforeRequestAsyncTest() + public async Task Utf16ChunkedTest() { var apiAction = new DefaultApiActionDescriptor(typeof(IMyApi).GetMethod("PostAsync")!); var context = new TestRequestContext(apiAction, new @@ -25,9 +25,32 @@ public async Task BeforeRequestAsyncTest() context.HttpContext.RequestMessage.RequestUri = new Uri("http://www.webapi.com/"); context.HttpContext.RequestMessage.Method = HttpMethod.Post; - var attr = new JsonContentAttribute() { CharSet = "utf-16" }; + var attr = new JsonContentAttribute() { CharSet = "utf-16", AllowChunked = true }; await attr.OnRequestAsync(new ApiParameterContext(context, 0)); + var body = await context.HttpContext.RequestMessage.Content!.ReadAsUtf8ByteArrayAsync(); + var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; + using var buffer = new RecyclableBufferWriter(); + JsonBufferSerializer.Serialize(buffer, context.Arguments[0], options); + var target = buffer.WrittenSpan.ToArray(); + Assert.True(body.SequenceEqual(target)); + } + + [Fact] + public async Task Utf8UnChunkedTest() + { + var apiAction = new DefaultApiActionDescriptor(typeof(IMyApi).GetMethod("PostAsync")!); + var context = new TestRequestContext(apiAction, new + { + name = "laojiu", + birthDay = DateTime.Parse("2010-10-10") + }); + + context.HttpContext.RequestMessage.RequestUri = new Uri("http://www.webapi.com/"); + context.HttpContext.RequestMessage.Method = HttpMethod.Post; + + var attr = new JsonContentAttribute() { CharSet = "utf-8", AllowChunked = false }; + await attr.OnRequestAsync(new ApiParameterContext(context, 0)); var body = await context.HttpContext.RequestMessage.Content!.ReadAsUtf8ByteArrayAsync(); var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; diff --git a/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs b/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs index bb820300..4cab65e6 100644 --- a/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs +++ b/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs @@ -1,15 +1,18 @@ using System; using System.Net.Http; +using System.Text; using System.Threading.Tasks; +using System.Xml; using WebApiClientCore.Attributes; using WebApiClientCore.Implementations; +using WebApiClientCore.Internals; using WebApiClientCore.Serialization; using Xunit; namespace WebApiClientCore.Test.Attributes.ParameterAttributes { public class XmlContentAttributeTest - { + { public class Model { public string? name { get; set; } @@ -29,12 +32,12 @@ public async Task OnRequestAsyncTest() context.HttpContext.RequestMessage.RequestUri = new Uri("http://www.webapi.com/"); context.HttpContext.RequestMessage.Method = HttpMethod.Post; - + var attr = new XmlContentAttribute(); await attr.OnRequestAsync(new ApiParameterContext(context, 0)); - var body = await context.HttpContext.RequestMessage.Content!.ReadAsStringAsync(); - var target = XmlSerializer.Serialize(context.Arguments[0],null); + + var target = XmlSerializer.Serialize(context.Arguments[0], null); Assert.True(body == target); } } diff --git a/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs b/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs index 2e9e8a3e..8b8d0984 100644 --- a/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs +++ b/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs @@ -1,4 +1,7 @@ -using Xunit; +using System; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using Xunit; namespace WebApiClientCore.Test.BuildinExtensions { @@ -9,6 +12,14 @@ public void ToHeaderNameTest() { Assert.Equal("Accept", HttpRequestHeader.Accept.ToHeaderName()); Assert.Equal("Accept-Charset", HttpRequestHeader.AcceptCharset.ToHeaderName()); + + foreach (var item in Enum.GetValues()) + { + var name = Enum.GetName(item); + var field = typeof(HttpRequestHeader).GetField(name!); + var headerName = field?.GetCustomAttribute()?.Name; + Assert.Equal(headerName, item.ToHeaderName()); + } } } } \ No newline at end of file diff --git a/WebApiClientCore.Test/BuildinExtensions/StringExtensionsTest.cs b/WebApiClientCore.Test/BuildinExtensions/StringExtensionsTest.cs index 2af669b0..0fa3a195 100644 --- a/WebApiClientCore.Test/BuildinExtensions/StringExtensionsTest.cs +++ b/WebApiClientCore.Test/BuildinExtensions/StringExtensionsTest.cs @@ -8,26 +8,26 @@ public class StringExtensionsTest public void RepaceIgnoreCaseTest() { var str = "WebApiClientCore.Benchmarks.StringReplaces.WebApiClientCore"; - var newStr = str.RepaceIgnoreCase("core", "CORE", out var replaced); + var newStr = str.ReplaceIgnoreCase("core", "CORE", out var replaced); Assert.True(replaced); Assert.Equal("WebApiClientCORE.Benchmarks.StringReplaces.WebApiClientCORE", newStr); str = "AbccBAd"; - var newStr2 = str.RepaceIgnoreCase("A", "x", out replaced); + var newStr2 = str.ReplaceIgnoreCase("A", "x", out replaced); Assert.True(replaced); Assert.Equal("xbccBxd", newStr2); str = "abc"; - var newStr3 = str.RepaceIgnoreCase("x", "x", out replaced); + var newStr3 = str.ReplaceIgnoreCase("x", "x", out replaced); Assert.False(replaced); Assert.Equal(str, newStr3); str = "aaa"; - var newStr4 = str.RepaceIgnoreCase("A", "x", out replaced); + var newStr4 = str.ReplaceIgnoreCase("A", "x", out replaced); Assert.True(replaced); Assert.Equal("xxx", newStr4); - var newStr5 = str.RepaceIgnoreCase("a", null, out replaced); + var newStr5 = str.ReplaceIgnoreCase("a", null, out replaced); Assert.True(replaced); Assert.Equal("", newStr5); } diff --git a/WebApiClientCore.Test/HttpApiTest.cs b/WebApiClientCore.Test/HttpApiTest.cs index 895a2cda..0d622b95 100644 --- a/WebApiClientCore.Test/HttpApiTest.cs +++ b/WebApiClientCore.Test/HttpApiTest.cs @@ -33,6 +33,13 @@ public interface IMyApi : IGet, IPost ITask Delete(); } + [Fact] + public void GetNameTest() + { + var name = HttpApi.GetName(typeof(HttpApiTest)); + Assert.Equal(typeof(HttpApiTest).FullName, name); + } + [Fact] public void GetAllApiMethodsTest() { diff --git a/WebApiClientCore.Test/HttpContents/JsonContentTest.cs b/WebApiClientCore.Test/HttpContents/JsonContentTest.cs new file mode 100644 index 00000000..9e637549 --- /dev/null +++ b/WebApiClientCore.Test/HttpContents/JsonContentTest.cs @@ -0,0 +1,31 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using WebApiClientCore.HttpContents; +using Xunit; + +namespace WebApiClientCore.Test.HttpContents +{ + public class JsonContentTest + { + [Fact] + public async Task Utf8JsonTest() + { + var options = new WebApiClientCore.HttpApiOptions(); + var content = new JsonContent("at我", options.JsonSerializeOptions); + Assert.Equal(content.GetEncoding(), Encoding.UTF8); + var text = await content.ReadAsStringAsync(); + Assert.Equal("\"at我\"", text); + } + + [Fact] + public async Task Utf16JsonTest() + { + var options = new WebApiClientCore.HttpApiOptions(); + var content = new JsonContent("at我", options.JsonSerializeOptions, Encoding.Unicode); + Assert.Equal(content.GetEncoding(), Encoding.Unicode); + var text = await content.ReadAsStringAsync(); + Assert.Equal("\"at我\"", text); + } + } +} diff --git a/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs b/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs index 3880dc86..3ef2ecb3 100644 --- a/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs +++ b/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs @@ -1,9 +1,7 @@ using System; -using System.Text; using System.Threading.Tasks; using WebApiClientCore.Exceptions; using WebApiClientCore.Implementations; -using WebApiClientCore.Internals; using Xunit; namespace WebApiClientCore.Test.Implementations @@ -112,11 +110,9 @@ public async Task AddFormFiledAsyncTest() [Fact] public async Task AddFormDataTextTest() { - string get(string name, string value) + static string get(string name, string value) { - return $@"Content-Disposition: form-data; name=""{name}"" - -{HttpUtil.UrlEncode(value, Encoding.UTF8)}"; + return $@"Content-Disposition: form-data; name=""{name}""{"\r\n\r\n"}{value}"; } var reqeust = new HttpApiRequestMessageImpl(); @@ -132,5 +128,19 @@ string get(string name, string value) Assert.Contains(get("age", "18"), body); Assert.Equal("multipart/form-data", reqeust.Content.Headers.ContentType!.MediaType); } + + + [Fact] + public void AddFormDataTextEmptyCollectionTest() + { + var reqeust = new HttpApiRequestMessageImpl + { + Method = System.Net.Http.HttpMethod.Post, + RequestUri = new Uri("http://webapiclient.com") + }; + + reqeust.AddFormDataText([]); + Assert.Null(reqeust.Content); + } } } diff --git a/WebApiClientCore.Test/Internals/LambdaTest.cs b/WebApiClientCore.Test/Internals/LambdaTest.cs index ce57f849..a6ec5d7c 100644 --- a/WebApiClientCore.Test/Internals/LambdaTest.cs +++ b/WebApiClientCore.Test/Internals/LambdaTest.cs @@ -21,21 +21,7 @@ public void GetTest() var name2 = getter2.Invoke(model); Assert.True(name2 == model.name); - Assert.NotNull(p.DeclaringType); - var getter3 = LambdaUtil.CreateGetFunc(p.DeclaringType, p.Name); - var name3 = getter2.Invoke(model).ToString(); - Assert.True(name3 == model.name); - - var kv = new KeyValuePair("k", 10); - var getter4 = LambdaUtil.CreateGetFunc(kv.GetType(), "Value"); - var value = (int)getter4.Invoke(kv); - Assert.True(value == kv.Value); - - var getter5 = LambdaUtil.CreateGetFunc(kv.GetType(), "Value"); - Assert.True(getter5.Invoke(kv) == kv.Value); - - var getter6 = LambdaUtil.CreateGetFunc(kv.GetType(), "Value"); - Assert.True(getter6.Invoke(kv) == kv.Value); + Assert.NotNull(p.DeclaringType); } [Fact] diff --git a/WebApiClientCore.Test/Serialization/JsonConverters/JsonCompatibleConverterTest.cs b/WebApiClientCore.Test/Serialization/JsonConverters/JsonCompatibleConverterTest.cs index 618612dd..3ea950a4 100644 --- a/WebApiClientCore.Test/Serialization/JsonConverters/JsonCompatibleConverterTest.cs +++ b/WebApiClientCore.Test/Serialization/JsonConverters/JsonCompatibleConverterTest.cs @@ -35,7 +35,16 @@ public void DateTimeReaderTest() var dateTime = JsonSerializer.Deserialize(json, options); Assert.Equal(DateTime.Parse("2010-10-10 10:10"), dateTime); } + [Fact] + public void DateTimeNullableReaderTest() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(JsonCompatibleConverter.DateTimeReader); + var json = "\"2010-10-10 10:10\""; + var dateTime = JsonSerializer.Deserialize(json, options); + Assert.Equal(DateTime.Parse("2010-10-10 10:10"), dateTime); + } [Fact] public void DateTimeOffsetReaderTest() { @@ -46,5 +55,16 @@ public void DateTimeOffsetReaderTest() var dateTime = JsonSerializer.Deserialize(json, options); Assert.Equal(DateTimeOffset.Parse("2010-10-10 10:10"), dateTime); } + + [Fact] + public void DateTimeOffsetNullableReaderTest() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(JsonCompatibleConverter.DateTimeReader); + + var json = "\"2010-10-10 10:10\""; + var dateTime = JsonSerializer.Deserialize(json, options); + Assert.Equal(DateTimeOffset.Parse("2010-10-10 10:10"), dateTime); + } } } diff --git a/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs b/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs index 7d3d560d..a8adfd00 100644 --- a/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs +++ b/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs @@ -12,43 +12,59 @@ public class KeyValueSerializerTest public void SerializeTest() { var obj1 = new FormatModel { Age = 18, Name = "lao九" }; - + var options = new HttpApiOptions().KeyValueSerializeOptions; var kvs = KeyValueSerializer.Serialize("pName", obj1, options) .ToDictionary(item => item.Key, item => item.Value, StringComparer.OrdinalIgnoreCase); - Assert.True(kvs.Count == 2); - Assert.True(kvs["Name"] == "lao九"); - Assert.True(kvs["Age"] == "18"); + Assert.Equal(2, kvs.Count); + Assert.Equal("lao九", kvs["Name"]); + Assert.Equal("18", kvs["Age"]); kvs = KeyValueSerializer.Serialize("pName", 30, null) .ToDictionary(item => item.Key, item => item.Value); - Assert.True(kvs.Count == 1); - Assert.True(kvs["pName"] == "30"); + Assert.Single(kvs); + Assert.Equal("30", kvs["pName"]); var bools = KeyValueSerializer.Serialize("bool", true, null); Assert.Equal("true", bools[0].Value); - var strings = KeyValueSerializer.Serialize("strings", "string", null); - Assert.Equal("string", strings[0].Value); + var strings = KeyValueSerializer.Serialize("strings", "\r\n", null); + Assert.Equal("\r\n", strings[0].Value); + var floats = KeyValueSerializer.Serialize("floats", 3.14f, null); + Assert.Equal("3.14", floats[0].Value); var dic = new System.Collections.Concurrent.ConcurrentDictionary(); dic.TryAdd("Key", "Value"); var kvs2 = KeyValueSerializer.Serialize("dic", dic, options); - Assert.True(kvs2.First().Key == "key"); + Assert.Equal("key", kvs2.First().Key); Assert.True(KeyValueSerializer.Serialize("null", null, null).Any()); } + + [Fact] + public void IgnoreNullValuesTest() + { + var obj1 = new FormatModel { Age = 18 }; + var options = new HttpApiOptions().KeyValueSerializeOptions; + options.IgnoreNullValues = true; + + var kvs = KeyValueSerializer.Serialize("pName", obj1, options) + .ToDictionary(item => item.Key, item => item.Value, StringComparer.OrdinalIgnoreCase); + + Assert.False(kvs.ContainsKey("name")); + } + [Fact] public void KeyNamingStyleTest() - { + { var model = new { x = new { y = 1 } }; var value = KeyValueSerializer.Serialize("root", model, null).First(); @@ -80,7 +96,7 @@ public void KeyNamingStyleTest() [Fact] public void ArrayIndexFormatTest() { - + var model = new { x = new { y = new[] { 1, 2 } } }; var kv = KeyValueSerializer.Serialize("root", model, new KeyValueSerializerOptions diff --git a/WebApiClientCore.Test/WebApiClientCore.Test.csproj b/WebApiClientCore.Test/WebApiClientCore.Test.csproj index 27dc2ea2..9745f1dc 100644 --- a/WebApiClientCore.Test/WebApiClientCore.Test.csproj +++ b/WebApiClientCore.Test/WebApiClientCore.Test.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0;net8.0 enable true Test.snk diff --git a/WebApiClientCore.sln b/WebApiClientCore.sln index 16ad76b8..df594692 100644 --- a/WebApiClientCore.sln +++ b/WebApiClientCore.sln @@ -27,6 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiClientCore.Extensions EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppAot", "AppAot\AppAot.csproj", "{F77DA016-1F63-46BF-A5A0-AD4662D528B9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{056F3910-2135-4D12-A420-6C3060533422}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apps", "Apps", "{E938B9B9-5F60-46BA-82D4-C7C30EBF6FF2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +89,12 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {518364A8-36D6-4927-929D-2CC26C099244} = {E938B9B9-5F60-46BA-82D4-C7C30EBF6FF2} + {3692CADD-3B12-4720-BB56-CE7511C69F9A} = {056F3910-2135-4D12-A420-6C3060533422} + {3371725B-C3BC-492C-B7A2-2B982EEF28AA} = {056F3910-2135-4D12-A420-6C3060533422} + {F77DA016-1F63-46BF-A5A0-AD4662D528B9} = {E938B9B9-5F60-46BA-82D4-C7C30EBF6FF2} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {915190C5-E5CB-440F-84B4-AE76368EA776} EndGlobalSection diff --git a/WebApiClientCore/ApiParameterContextExtensions.cs b/WebApiClientCore/ApiParameterContextExtensions.cs index 37308e46..aba8b119 100644 --- a/WebApiClientCore/ApiParameterContextExtensions.cs +++ b/WebApiClientCore/ApiParameterContextExtensions.cs @@ -1,6 +1,8 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Text.Json; using WebApiClientCore.Internals; using WebApiClientCore.Serialization; @@ -12,10 +14,12 @@ namespace WebApiClientCore public static class ApiParameterContextExtensions { /// - /// 序列化参数值为utf8编码的Json + /// 序列化参数值为 utf8 编码的 json /// /// /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static byte[] SerializeToJson(this ApiParameterContext context) { return context.SerializeToJson(Encoding.UTF8); @@ -27,6 +31,8 @@ public static byte[] SerializeToJson(this ApiParameterContext context) /// /// 编码 /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static byte[] SerializeToJson(this ApiParameterContext context, Encoding encoding) { using var bufferWriter = new RecyclableBufferWriter(); @@ -44,23 +50,60 @@ public static byte[] SerializeToJson(this ApiParameterContext context, Encoding } /// - /// 序列化参数值为utf8编码的Json + /// 序列化参数值为 utf8 编码的 json /// /// /// buffer写入器 + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static void SerializeToJson(this ApiParameterContext context, IBufferWriter bufferWriter) { var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; JsonBufferSerializer.Serialize(bufferWriter, context.ParameterValue, options); } + /// + /// 序列化参数值为 json 文本 + /// + /// + /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + public static string? SerializeToJsonString(this ApiParameterContext context) + { + var value = context.ParameterValue; + if (value == null) + { + return null; + } + + var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; + return JsonSerializer.Serialize(value, value.GetType(), options); + } + /// /// 序列化参数值为Xml /// /// /// xml的编码 /// + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] public static string? SerializeToXml(this ApiParameterContext context, Encoding? encoding) + { + using var bufferWriter = new RecyclableBufferWriter(); + context.SerializeToXml(encoding, bufferWriter); + return bufferWriter.WrittenSpan.ToString(); + } + + /// + /// 序列化参数值为Xml + /// + /// + /// 数据写入器 + /// xml的编码 + /// + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] + public static void SerializeToXml(this ApiParameterContext context, Encoding? encoding, IBufferWriter bufferWriter) { var options = context.HttpContext.HttpApiOptions.XmlSerializeOptions; if (encoding != null && encoding.Equals(options.Encoding) == false) @@ -69,7 +112,7 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit options.Encoding = encoding; } - return XmlSerializer.Serialize(context.ParameterValue, options); + XmlSerializer.Serialize(context.ParameterValue, options, bufferWriter); } /// @@ -77,6 +120,8 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit /// /// /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static IList SerializeToKeyValues(this ApiParameterContext context) { var options = context.HttpContext.HttpApiOptions.KeyValueSerializeOptions; diff --git a/WebApiClientCore/ApiRequestContextExtensions.cs b/WebApiClientCore/ApiRequestContextExtensions.cs index 6fd2f877..f9a3adfa 100644 --- a/WebApiClientCore/ApiRequestContextExtensions.cs +++ b/WebApiClientCore/ApiRequestContextExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using System; using System.Diagnostics.CodeAnalysis; -using System.Net.Http; +using System.Linq; namespace WebApiClientCore { @@ -81,39 +81,27 @@ public static bool TryGetArgument(this ApiRequestContext context, string paramet } /// - /// 返回请求使用的HttpCompletionOption + /// 获取以 ApiAction 为容器的 ILogger /// /// /// - internal static HttpCompletionOption GetCompletionOption(this ApiRequestContext context) + public static ILogger? GetActionLogger(this ApiRequestContext context) { - if (context.HttpContext.CompletionOption != null) - { - return context.HttpContext.CompletionOption.Value; - } - - var dataType = context.ActionDescriptor.Return.DataType; - return dataType.IsRawHttpResponseMessage || dataType.IsRawStream - ? HttpCompletionOption.ResponseHeadersRead - : HttpCompletionOption.ResponseContentRead; - } + return context.ActionDescriptor.Properties.GetOrAdd(typeof(ILogger), CreateLogger) as ILogger; - /// - /// 获取指向api方法名的日志 - /// - /// - /// - internal static ILogger? GetLogger(this ApiRequestContext context) - { - var loggerFactory = context.HttpContext.ServiceProvider.GetService(); - if (loggerFactory == null) + object? CreateLogger(object _) { - return null; - } + var loggerFactory = context.HttpContext.ServiceProvider.GetService(); + if (loggerFactory == null) + { + return null; + } - var method = context.ActionDescriptor.Member; - var categoryName = $"{method.DeclaringType?.Namespace}.{method.DeclaringType?.Name}.{method.Name}"; - return loggerFactory.CreateLogger(categoryName); + var action = context.ActionDescriptor; + var parameters = action.Parameters.Select(item => HttpApi.GetName(item.ParameterType, includeNamespace: false)); + var categoryName = $"{action.InterfaceType.FullName}.{action.Member.Name}({string.Join(", ", parameters)})"; + return loggerFactory.CreateLogger(categoryName); + } } } } diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 43eae54d..714ef208 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -1,7 +1,6 @@ using System; -using System.Net.Http; -using System.Text; -using System.Text.Json; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Json; using System.Threading.Tasks; using WebApiClientCore.Serialization; @@ -18,6 +17,8 @@ public static class ApiResponseContextExtensions /// /// 目标类型 /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static async Task JsonDeserializeAsync(this ApiResponseContext context, Type objType) { var response = context.HttpContext.ResponseMessage; @@ -27,32 +28,8 @@ public static class ApiResponseContextExtensions } var content = response.Content; - var encoding = content.GetEncoding(); var options = context.HttpContext.HttpApiOptions.JsonDeserializeOptions; - - if (Encoding.UTF8.Equals(encoding) == false) - { - var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - if (byteArray.Length == 0) - { - return objType.DefaultValue(); - } - var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); - return JsonSerializer.Deserialize(utf8Json, objType, options); - } - - if (content.IsBuffered() == false) - { - var utf8Json = await content.ReadAsStreamAsync().ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(utf8Json, objType, options).ConfigureAwait(false); - } - else - { - var utf8Json = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - return utf8Json.Length == 0 - ? objType.DefaultValue() - : JsonSerializer.Deserialize(utf8Json, objType, options); - } + return await content.ReadFromJsonAsync(objType, options, context.RequestAborted); } /// @@ -61,6 +38,7 @@ public static class ApiResponseContextExtensions /// /// 目标类型 /// + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] public static async Task XmlDeserializeAsync(this ApiResponseContext context, Type objType) { var response = context.HttpContext.ResponseMessage; @@ -71,7 +49,7 @@ public static class ApiResponseContextExtensions var content = response.Content; var options = context.HttpContext.HttpApiOptions.XmlDeserializeOptions; - var xml = await content.ReadAsStringAsync().ConfigureAwait(false); + var xml = await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false); return XmlSerializer.Deserialize(xml, objType, options); } } diff --git a/WebApiClientCore/Attributes/ActionAttributes/FormDataTextAttribute.cs b/WebApiClientCore/Attributes/ActionAttributes/FormDataTextAttribute.cs index dab46676..80f64cad 100644 --- a/WebApiClientCore/Attributes/ActionAttributes/FormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ActionAttributes/FormDataTextAttribute.cs @@ -4,7 +4,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示参数值作为multipart/form-data表单的一个文本项 + /// 表示参数值作为 multipart/form-data 表单的一个文本项 /// public partial class FormDataTextAttribute : ApiActionAttribute { @@ -19,7 +19,7 @@ public partial class FormDataTextAttribute : ApiActionAttribute private readonly string? value; /// - /// 表示name和value写入multipart/form-data表单 + /// 表示 name 和 value 写入 multipart/form-data 表单 /// /// 字段名称 /// 字段的值 diff --git a/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs b/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs index 6f10edd2..734e7d24 100644 --- a/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs @@ -4,7 +4,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示参数值作为x-www-form-urlencoded的字段 + /// 表示参数值作为 x-www-form-urlencoded 的字段 /// public partial class FormFieldAttribute : ApiActionAttribute { @@ -19,7 +19,7 @@ public partial class FormFieldAttribute : ApiActionAttribute private readonly string? value; /// - /// 表示name和value写入x-www-form-urlencoded表单 + /// 表示 name 和 value 写入 x-www-form-urlencoded 表单 /// /// 字段名称 /// 字段的值 @@ -36,9 +36,9 @@ public FormFieldAttribute(string name, object? value) /// /// 上下文 /// - public override async Task OnRequestAsync(ApiRequestContext context) + public override Task OnRequestAsync(ApiRequestContext context) { - await context.HttpContext.RequestMessage.AddFormFieldAsync(this.name, this.value).ConfigureAwait(false); + return context.HttpContext.RequestMessage.AddFormFieldAsync(this.name, this.value); } } } diff --git a/WebApiClientCore/Attributes/ActionAttributes/HeaderAttribute.cs b/WebApiClientCore/Attributes/ActionAttributes/HeaderAttribute.cs index eb97545c..6569738e 100644 --- a/WebApiClientCore/Attributes/ActionAttributes/HeaderAttribute.cs +++ b/WebApiClientCore/Attributes/ActionAttributes/HeaderAttribute.cs @@ -25,6 +25,7 @@ public partial class HeaderAttribute : ApiActionAttribute /// /// header名称 /// header值 + /// [AttributeCtorUsage(AttributeTargets.Interface | AttributeTargets.Method)] public HeaderAttribute(HttpRequestHeader name, string value) : this(name.ToHeaderName(), value) diff --git a/WebApiClientCore/Attributes/ActionAttributes/HttpHostAttribute.cs b/WebApiClientCore/Attributes/ActionAttributes/HttpHostAttribute.cs index 01755351..fb47ec8e 100644 --- a/WebApiClientCore/Attributes/ActionAttributes/HttpHostAttribute.cs +++ b/WebApiClientCore/Attributes/ActionAttributes/HttpHostAttribute.cs @@ -5,8 +5,8 @@ namespace WebApiClientCore.Attributes { /// - /// 表示请求服务http绝对完整主机域名 - /// 例如http://www.abc.com/ + /// 表示请求服务绝对完整主机域名 + /// 例如 http://www.abc.com/ /// [DebuggerDisplay("Host = {Host}")] public class HttpHostAttribute : HttpHostBaseAttribute @@ -17,9 +17,9 @@ public class HttpHostAttribute : HttpHostBaseAttribute public Uri Host { get; } /// - /// 请求服务http绝对完整主机域名 + /// 请求服务绝对完整主机域名 /// - /// 例如http://www.abc.com + /// 例如 http://www.abc.com /// /// public HttpHostAttribute(string host) diff --git a/WebApiClientCore/Attributes/ActionAttributes/HttpMethodAttribute.cs b/WebApiClientCore/Attributes/ActionAttributes/HttpMethodAttribute.cs index 02ec216e..7ca68aaa 100644 --- a/WebApiClientCore/Attributes/ActionAttributes/HttpMethodAttribute.cs +++ b/WebApiClientCore/Attributes/ActionAttributes/HttpMethodAttribute.cs @@ -6,7 +6,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示http请求方法描述特性 + /// 表示 http 请求方法描述特性 /// [DebuggerDisplay("Method = {Method}")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs b/WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs index 1b457384..8beadcc1 100644 --- a/WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs +++ b/WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs @@ -22,7 +22,7 @@ public class CacheAttribute : ApiCacheAttribute /// /// 获取缓存键的请求头名称 /// - protected string[] IncludeHeaderNames { get; private set; } = Array.Empty(); + protected string[] IncludeHeaderNames { get; private set; } = []; /// /// 获取或设置连同作为缓存键的请求头名称 diff --git a/WebApiClientCore/Attributes/FilterAttributes/LogMessage.cs b/WebApiClientCore/Attributes/FilterAttributes/LogMessage.cs index 43b5f6e8..74ee2ce8 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LogMessage.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LogMessage.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.IO; using System.Text; @@ -9,6 +10,26 @@ namespace WebApiClientCore.Attributes /// public class LogMessage { + private static readonly Action logInformation = LoggerMessage.Define(LogLevel.Information, 0, + """ + [REQUEST] + {request} + [RESPONSE] + {response} + [ELAPSED] + {elapsed} + """); + + private static readonly Action logError = LoggerMessage.Define(LogLevel.Error, 0, + """ + [REQUEST] + {request} + [RESPONSE] + {response} + [ELAPSED] + {elapsed} + """); + /// /// 获取或设置是否记录请求 /// @@ -108,6 +129,47 @@ public string ToIndentedString(int spaceCount) return builder.ToString(); } + /// + /// 写到日志组件 + /// + /// 日志组件 + public void WriteTo(ILogger logger) + { + var builder = new TextBuilder(); + var request = "..."; + var response = "..."; + var elapsed = this.ResponseTime.Subtract(this.RequestTime); + + if (this.HasRequest) + { + request = builder + .Clear() + .AppendIfNotNull(this.RequestHeaders) + .AppendLineIf(this.RequestContent != null) + .AppendIfNotNull(this.RequestContent) + .ToString(); + } + + if (this.HasResponse) + { + response = builder + .Clear() + .AppendIfNotNull(this.ResponseHeaders) + .AppendLineIf(this.ResponseContent != null) + .AppendIfNotNull(this.ResponseContent) + .ToString(); + } + + if (this.Exception == null) + { + logInformation(logger, request, response, elapsed, null); + } + else + { + logError(logger, request, response, elapsed, this.Exception); + } + } + /// /// 转换为字符串 /// @@ -236,6 +298,12 @@ public TextBuilder AppendLineIfNotNull(string? value) return this; } + public TextBuilder Clear() + { + this.builder.Clear(); + return this; + } + /// /// 转换为字符串 /// diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index 4cd4bbd3..578d2a20 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading.Tasks; using WebApiClientCore.HttpContents; @@ -11,6 +12,8 @@ namespace WebApiClientCore.Attributes /// public class LoggingFilterAttribute : ApiFilterAttribute { + private readonly bool isLoggingFilterAttribute; + /// /// 获取或设置是否输出请求内容 /// @@ -27,6 +30,7 @@ public class LoggingFilterAttribute : ApiFilterAttribute public LoggingFilterAttribute() { this.OrderIndex = int.MaxValue; + this.isLoggingFilterAttribute = this.GetType() == typeof(LoggingFilterAttribute); } /// @@ -41,20 +45,27 @@ public sealed async override Task OnRequestAsync(ApiRequestContext context) return; } + // 本类型依赖于 ActionLogger + // 且 LogLevel 最大为 LogLevel.Error + if (this.isLoggingFilterAttribute && !IsLogEnable(context)) + { + return; + } + var logMessage = new LogMessage { RequestTime = DateTime.Now, HasRequest = this.LogRequest }; - if (this.LogRequest == true) + if (this.LogRequest) { var request = context.HttpContext.RequestMessage; logMessage.RequestHeaders = request.GetHeadersString(); logMessage.RequestContent = await ReadRequestContentAsync(request).ConfigureAwait(false); } - context.Properties.Set(typeof(LoggingFilterAttribute), logMessage); + context.Properties.Set(typeof(LogMessage), logMessage); } /// @@ -64,17 +75,80 @@ public sealed async override Task OnRequestAsync(ApiRequestContext context) /// public sealed async override Task OnResponseAsync(ApiResponseContext context) { - if (context.HttpContext.HttpApiOptions.UseLogging == false) + var logMessage = context.Properties.Get(typeof(LogMessage)); + if (logMessage == null) { return; } - var logMessage = context.Properties.Get(typeof(LoggingFilterAttribute)); - if (logMessage == null) + if (this.isLoggingFilterAttribute) { - return; + if (IsLogEnable(context, out var logger)) + { + await this.FillResponseAsync(logMessage, context).ConfigureAwait(false); + logMessage.WriteTo(logger); + } + } + else + { + await this.FillResponseAsync(logMessage, context).ConfigureAwait(false); + await this.WriteLogAsync(context, logMessage).ConfigureAwait(false); + } + } + + /// + /// 写日志到指定日志组件 + /// 默认写入Microsoft.Extensions.Logging + /// + /// 上下文 + /// 日志消息 + /// + protected virtual Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage) + { + var logger = context.GetActionLogger(); + if (logger != null) + { + this.WriteLog(logger, logMessage); + } + return Task.CompletedTask; + } + + /// + /// 写日志到ILogger + /// + /// 日志组件 + /// 日志消息 + protected virtual void WriteLog(ILogger logger, LogMessage logMessage) + { + logMessage.WriteTo(logger); + } + + private static bool IsLogEnable(ApiRequestContext context) + { + var logger = context.GetActionLogger(); + return logger != null && logger.IsEnabled(LogLevel.Error); + } + + private static bool IsLogEnable(ApiResponseContext context, [MaybeNullWhen(false)] out ILogger logger) + { + logger = context.GetActionLogger(); + if (logger == null) + { + return false; } + var logLevel = context.Exception == null ? LogLevel.Information : LogLevel.Error; + return logger.IsEnabled(logLevel); + } + + /// + /// 填充响应内容到LogMessage + /// + /// + /// + /// + private async Task FillResponseAsync(LogMessage logMessage, ApiResponseContext context) + { logMessage.ResponseTime = DateTime.Now; logMessage.Exception = context.Exception; @@ -85,8 +159,6 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) logMessage.ResponseHeaders = response.GetHeadersString(); logMessage.ResponseContent = await ReadResponseContentAsync(context).ConfigureAwait(false); } - - await this.WriteLogAsync(context, logMessage).ConfigureAwait(false); } /// @@ -94,15 +166,15 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) /// /// /// - private static async Task ReadRequestContentAsync(HttpApiRequestMessage request) + private static async ValueTask ReadRequestContentAsync(HttpApiRequestMessage request) { if (request.Content == null) { return null; } - return request.Content is ICustomHttpContentConvertable convertable - ? await convertable.ToCustomHttpContext().ReadAsStringAsync().ConfigureAwait(false) + return request.Content is ICustomHttpContentConvertable conversable + ? await conversable.ToCustomHttpContext().ReadAsStringAsync().ConfigureAwait(false) : await request.Content.ReadAsStringAsync().ConfigureAwait(false); } @@ -119,46 +191,9 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) return null; } - if (content.IsBuffered() == true || context.GetCompletionOption() == HttpCompletionOption.ResponseContentRead) - { - return await content.ReadAsStringAsync().ConfigureAwait(false); - } - - return "..."; - } - - /// - /// 写日志到指定日志组件 - /// 默认写入Microsoft.Extensions.Logging - /// - /// 上下文 - /// 日志消息 - /// - protected virtual Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage) - { - var logger = context.GetLogger(); - if (logger != null) - { - this.WriteLog(logger, logMessage); - } - return Task.CompletedTask; - } - - /// - /// 写日志到ILogger - /// - /// 日志 - /// 日志消息 - protected virtual void WriteLog(ILogger logger, LogMessage logMessage) - { - if (logMessage.Exception == null) - { - logger.LogInformation(logMessage.ToString()); - } - else - { - logger.LogError(logMessage.ToString()); - } + return content.IsBuffered() == true + ? await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false) + : "..."; } } } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs index 9273b2da..6f8be3b7 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace WebApiClientCore.Attributes { /// - /// 使用KeyValueSerializer序列化参数值得到的键值对作为x-www-form-urlencoded表单 + /// 使用KeyValueSerializer序列化参数值得到的键值对作为 x-www-form-urlencoded 表单 /// public class FormContentAttribute : HttpContentAttribute, ICollectionFormatable { @@ -14,9 +15,11 @@ public class FormContentAttribute : HttpContentAttribute, ICollectionFormatable public CollectionFormat CollectionFormat { get; set; } = CollectionFormat.Multi; /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] protected override async Task SetHttpContentAsync(ApiParameterContext context) { var keyValues = this.SerializeToKeyValues(context).CollectAs(this.CollectionFormat); @@ -24,10 +27,12 @@ protected override async Task SetHttpContentAsync(ApiParameterContext context) } /// - /// 序列化参数为keyValue + /// 序列化参数为 keyValue 集合 /// /// 上下文 /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs index 73808f75..713bdb07 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace WebApiClientCore.Attributes { /// - /// 使用KeyValueSerializer序列化参数值得到的键值作为multipart/form-data表单 + /// 使用KeyValueSerializer序列化参数值得到的键值作为 multipart/form-data 表单 /// public class FormDataContentAttribute : HttpContentAttribute, ICollectionFormatable { @@ -14,7 +15,7 @@ public class FormDataContentAttribute : HttpContentAttribute, ICollectionFormata public CollectionFormat CollectionFormat { get; set; } = CollectionFormat.Multi; /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// @@ -26,10 +27,12 @@ protected override Task SetHttpContentAsync(ApiParameterContext context) } /// - /// 序列化参数为keyValue + /// 序列化参数为 keyValue 集合 /// /// 上下文 - /// + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormDataTextAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormDataTextAttribute.cs index 459b33d1..1608d96e 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormDataTextAttribute.cs @@ -4,13 +4,13 @@ namespace WebApiClientCore.Attributes { /// - /// 表示参数值作为multipart/form-data表单的一个文本项 + /// 表示参数值作为 multipart/form-data 表单的一个文本项 /// [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public partial class FormDataTextAttribute : IApiParameterAttribute { /// - /// 表示参数值作为multipart/form-data表单的一个文本项 + /// 表示参数值作为 multipart/form-data 表单的一个文本项 /// [AttributeCtorUsage(AttributeTargets.Parameter)] public FormDataTextAttribute() diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormFieldAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormFieldAttribute.cs index c2b5f42f..ee1abdd6 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormFieldAttribute.cs @@ -4,13 +4,13 @@ namespace WebApiClientCore.Attributes { /// - /// 表示参数值作为x-www-form-urlencoded的字段 + /// 表示参数值作为 x-www-form-urlencoded 的字段 /// [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public partial class FormFieldAttribute : IApiParameterAttribute { /// - /// 表示参数值作为x-www-form-urlencoded的字段 + /// 表示参数值作为 x-www-form-urlencoded 的字段 /// [AttributeCtorUsage(AttributeTargets.Parameter)] public FormFieldAttribute() diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HeaderAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HeaderAttribute.cs index 8374cc30..0d073397 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HeaderAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HeaderAttribute.cs @@ -26,6 +26,7 @@ public HeaderAttribute() /// 将参数值设置到Header /// /// header别名 + /// [AttributeCtorUsage(AttributeTargets.Parameter)] public HeaderAttribute(HttpRequestHeader aliasName) : this(aliasName.ToHeaderName()) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs index f260069d..09f6d5e0 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using WebApiClientCore.Exceptions; namespace WebApiClientCore.Attributes @@ -11,7 +12,7 @@ public class HeadersAttribute : ApiParameterAttribute { /// /// 获取或设置是否将请求名的_转换为- - /// 默认为true + /// 默认为 true /// public bool UnderlineToMinus { get; set; } = true; @@ -21,6 +22,8 @@ public class HeadersAttribute : ApiParameterAttribute /// 上下文 /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public override Task OnRequestAsync(ApiParameterContext context) { foreach (var item in context.SerializeToKeyValues()) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs index c2476d4b..471f81e6 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using System; using System.Net.Http; using System.Threading.Tasks; using WebApiClientCore.Exceptions; @@ -10,6 +11,8 @@ namespace WebApiClientCore.Attributes /// public abstract class HttpContentAttribute : ApiParameterAttribute { + private static readonly Action logWarning = LoggerMessage.Define(LogLevel.Warning, 0, "{message}"); + /// /// http请求之前 /// @@ -21,15 +24,18 @@ public sealed override async Task OnRequestAsync(ApiParameterContext context) var method = context.HttpContext.RequestMessage.Method; if (method == HttpMethod.Get || method == HttpMethod.Head) { - var logger = context.GetLogger(); - logger?.LogWarning(Resx.gethead_Content_Warning.Format(method)); + var logger = context.GetActionLogger(); + if (logger != null) + { + logWarning(logger, Resx.gethead_Content_Warning.Format(method), null); + } } await this.SetHttpContentAsync(context).ConfigureAwait(false); } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs index fba6dd0f..ff3284f2 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -1,20 +1,29 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Text; using System.Threading.Tasks; -using WebApiClientCore.HttpContents; namespace WebApiClientCore.Attributes { /// - /// 使用JsonSerializer序列化参数值得到的json文本作为application/json请求 + /// 使用JsonSerializer序列化参数值得到的 json 文本作为 application/json 请求 /// 每个Api只能注明于其中的一个参数 /// - public class JsonContentAttribute : HttpContentAttribute, ICharSetable + public class JsonContentAttribute : HttpContentAttribute, ICharSetable, IChunkedable { + private const string jsonMediaType = "application/json"; + private static readonly MediaTypeHeaderValue defaultMediaType = new(jsonMediaType); + + private MediaTypeHeaderValue mediaType = defaultMediaType; + private Encoding encoding = Encoding.UTF8; + /// - /// 编码方式 + /// 获取或设置是否允许 chunked 传输 + /// 默认为 true /// - private Encoding encoding = Encoding.UTF8; + public bool AllowChunked { get; set; } = true; /// /// 获取或设置编码名称 @@ -23,18 +32,34 @@ public class JsonContentAttribute : HttpContentAttribute, ICharSetable public string CharSet { get => this.encoding.WebName; - set => this.encoding = Encoding.GetEncoding(value); + set + { + this.encoding = Encoding.GetEncoding(value); + this.mediaType = new MediaTypeHeaderValue(jsonMediaType) { CharSet = this.encoding.WebName }; + } } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] protected override Task SetHttpContentAsync(ApiParameterContext context) { + var value = context.ParameterValue; var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = new JsonContent(context.ParameterValue, options, this.encoding); + + if (this.AllowChunked) + { + var valueType = value == null ? context.Parameter.ParameterType : value.GetType(); + context.HttpContext.RequestMessage.Content = JsonContent.Create(value, valueType, this.mediaType, options); + } + else + { + context.HttpContext.RequestMessage.Content = new HttpContents.JsonContent(value, options, this.encoding); + } return Task.CompletedTask; } } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs index 50ef7105..654125ca 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs @@ -1,10 +1,10 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace WebApiClientCore.Attributes { /// - /// 表示参数值序列化为Json并作为multipart/form-data表单的一个文本项 + /// 表示参数值序列化为 json 并作为 multipart/form-data 表单的一个文本项 /// public class JsonFormDataTextAttribute : ApiParameterAttribute { @@ -13,13 +13,13 @@ public class JsonFormDataTextAttribute : ApiParameterAttribute /// /// 上下文 /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public override Task OnRequestAsync(ApiParameterContext context) { - var json = context.SerializeToJson(); var fieldName = context.ParameterName; - var fieldValue = Encoding.UTF8.GetString(json); + var fieldValue = context.SerializeToJsonString(); context.HttpContext.RequestMessage.AddFormDataText(fieldName, fieldValue); - return Task.CompletedTask; } } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs index 8d89fb76..3647b0ed 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs @@ -1,10 +1,10 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace WebApiClientCore.Attributes { /// - /// 表示参数值序列化为Json并作为x-www-form-urlencoded的字段 + /// 表示参数值序列化为 json 并作为 x-www-form-urlencoded 的字段 /// public class JsonFormFieldAttribute : ApiParameterAttribute { @@ -13,11 +13,12 @@ public class JsonFormFieldAttribute : ApiParameterAttribute /// /// 上下文 /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public override async Task OnRequestAsync(ApiParameterContext context) - { - var json = context.SerializeToJson(); + { var fieldName = context.ParameterName; - var fieldValue = Encoding.UTF8.GetString(json); + var fieldValue = context.SerializeToJsonString(); await context.HttpContext.RequestMessage.AddFormFieldAsync(fieldName, fieldValue).ConfigureAwait(false); } } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs index 221fb13f..c7fb01cf 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -7,7 +8,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用KeyValueSerializer序列化参数值得到的键值对作为url路径参数或query参数的特性 + /// 使用KeyValueSerializer序列化参数值得到的键值对作为 url 路径参数或 query 参数的特性 /// public class PathQueryAttribute : ApiParameterAttribute, ICollectionFormatable { @@ -22,6 +23,8 @@ public class PathQueryAttribute : ApiParameterAttribute, ICollectionFormatable /// 上下文 /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public override Task OnRequestAsync(ApiParameterContext context) { var uri = context.HttpContext.RequestMessage.RequestUri; @@ -36,19 +39,21 @@ public override Task OnRequestAsync(ApiParameterContext context) } /// - /// 序列化参数为keyValue + /// 序列化参数为 keyValue 集合 /// /// 上下文 /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); } /// - /// 创建新的uri + /// 创建新的 uri /// - /// 原始uri + /// 原始 uri /// 键值对 /// protected virtual Uri CreateUri(Uri uri, IEnumerable keyValues) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/RawFormContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/RawFormContentAttribute.cs index ae505725..9aec5a55 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/RawFormContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/RawFormContentAttribute.cs @@ -9,7 +9,7 @@ namespace WebApiClientCore.Attributes public class RawFormContentAttribute : HttpContentAttribute { /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/RawJsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/RawJsonContentAttribute.cs index d4eb5dfa..05ffb945 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/RawJsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/RawJsonContentAttribute.cs @@ -3,12 +3,12 @@ namespace WebApiClientCore.Attributes { /// - /// 表示将参数的文本内容作为请求json内容 + /// 表示将参数的文本内容作为请求 json 内容 /// public class RawJsonContentAttribute : RawStringContentAttribute { /// - /// 将参数的文本内容作为请求json内容 + /// 将参数的文本内容作为请求 json 内容 /// public RawJsonContentAttribute() : base(JsonContent.MediaType) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/RawStringContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/RawStringContentAttribute.cs index eb2cc4f0..cc0ec9b5 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/RawStringContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/RawStringContentAttribute.cs @@ -41,7 +41,7 @@ public RawStringContentAttribute(string mediaType) } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/RawXmlContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/RawXmlContentAttribute.cs index 93e7a3d4..68c66c7b 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/RawXmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/RawXmlContentAttribute.cs @@ -3,12 +3,12 @@ namespace WebApiClientCore.Attributes { /// - /// 表示将参数的文本内容作为请求xml内容 + /// 表示将参数的文本内容作为请求 xml 内容 /// public class RawXmlContentAttribute : RawStringContentAttribute { /// - /// 将参数的文本内容作为请求xml内容 + /// 将参数的文本内容作为请求 xml 内容 /// public RawXmlContentAttribute() : base(XmlContent.MediaType) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/UriAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/UriAttribute.cs index 85cca7d4..63afcbc3 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/UriAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/UriAttribute.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示将参数值作为请求uri的特性 + /// 表示将参数值作为请求 Uri 的特性 /// 要求必须修饰于第一个参数 /// 支持绝对或相对路径 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs index b56e050f..fbc0bd2e 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs @@ -1,12 +1,14 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Threading.Tasks; using WebApiClientCore.HttpContents; +using WebApiClientCore.Internals; namespace WebApiClientCore.Attributes { /// - /// 使用XmlSerializer序列化参数值得到的xml文本作为application/xml请求 + /// 使用XmlSerializer序列化参数值得到的 xml 文本作为 application/xml 请求 /// public class XmlContentAttribute : HttpContentAttribute, ICharSetable { @@ -26,14 +28,16 @@ public string CharSet } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] protected override Task SetHttpContentAsync(ApiParameterContext context) { - var xml = context.SerializeToXml(this.encoding); - context.HttpContext.RequestMessage.Content = new XmlContent(xml, this.encoding); + using var bufferWriter = new RecyclableBufferWriter(); + context.SerializeToXml(this.encoding, bufferWriter); + context.HttpContext.RequestMessage.Content = new XmlContent(bufferWriter.WrittenSpan, this.encoding); return Task.CompletedTask; } } diff --git a/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs index 1f516c60..a9ca64ee 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs @@ -43,26 +43,26 @@ public virtual int OrderIndex public bool AllowMultiple => this.GetType().IsAllowMultiple(); /// - /// 获取或设置是否确保响应的http状态码通过IsSuccessStatusCode验证 - /// 当值为true时,请求可能会引发HttpStatusFailureException - /// 默认为true + /// 获取或设置是否确保响应的 http 状态码通过IsSuccessStatusCode验证 + /// 当值为 true 时,请求可能会引发HttpStatusFailureException + /// 默认为 true /// public bool EnsureSuccessStatusCode { get; set; } = true; /// - /// 获取或设置是否确保响应的ContentType与指定的Accpet-ContentType一致 - /// 默认为true + /// 获取或设置是否确保响应的ContentType与指定的 Accept-ContentType 一致 + /// 默认为 false /// - public bool EnsureMatchAcceptContentType { get; set; } = true; + public bool EnsureMatchAcceptContentType { get; set; } = false; /// /// 响应内容处理的抽象特性 /// - /// 收受的内容类型 + /// 收受的内容类型 /// - public ApiReturnAttribute(MediaTypeWithQualityHeaderValue accpetContentType) + public ApiReturnAttribute(MediaTypeWithQualityHeaderValue acceptContentType) { - this.AcceptContentType = accpetContentType ?? throw new ArgumentNullException(nameof(accpetContentType)); + this.AcceptContentType = acceptContentType ?? throw new ArgumentNullException(nameof(acceptContentType)); } /// @@ -98,10 +98,10 @@ public async Task OnResponseAsync(ApiResponseContext context) return; } - var contenType = response.Content.Headers.ContentType; - if (contenType != null + var contentType = response.Content.Headers.ContentType; + if (contentType != null && this.EnsureMatchAcceptContentType - && this.IsMatchAcceptContentType(contenType) == false) + && this.IsMatchAcceptContentType(contentType) == false) { return; } @@ -113,7 +113,7 @@ public async Task OnResponseAsync(ApiResponseContext context) /// /// 指示响应的ContentType与AcceptContentType是否匹配 - /// 返回false则调用下一个ApiReturnAttribute来处理响应结果 + /// 返回 false 则调用下一个ApiReturnAttribute来处理响应结果 /// /// 响应的ContentType /// @@ -140,7 +140,7 @@ protected virtual Task ValidateResponseAsync(ApiResponseContext context) } /// - /// 验证响应消息的http状态码 + /// 验证响应消息的 http 状态码 /// 验证不通过则抛出指定的异常 /// /// 响应消息 @@ -154,7 +154,7 @@ protected virtual void ValidateResponseStatusCode(HttpResponseMessage response) } /// - /// 指示http状态码是否为成功的状态码 + /// 指示 http 状态码是否为成功的状态码 /// /// http状态码 /// diff --git a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs index 3b1cb1df..34e1f245 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs @@ -1,24 +1,18 @@ -using System.Net.Http.Headers; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Threading.Tasks; using WebApiClientCore.HttpContents; -using WebApiClientCore.Internals; namespace WebApiClientCore.Attributes { /// - /// 表示json内容的结果特性 + /// 表示 json 内容的结果特性 /// public class JsonReturnAttribute : ApiReturnAttribute { - /// - /// text/json - /// - private static readonly string textJson = "text/json"; - - /// - /// 问题描述json - /// - private static readonly string problemJson = "application/problem+json"; + private static readonly HashSet allowMediaTypes = new([JsonContent.MediaType, "text/json", "application/problem+json"], StringComparer.OrdinalIgnoreCase); /// /// json内容的结果特性 @@ -39,22 +33,23 @@ public JsonReturnAttribute(double acceptQuality) /// /// 指示响应的ContentType与AcceptContentType是否匹配 - /// 返回false则调用下一个ApiReturnAttribute来处理响应结果 + /// 返回 false 则调用下一个ApiReturnAttribute来处理响应结果 /// /// 响应的ContentType /// protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseContentType) { - return base.IsMatchAcceptContentType(responseContentType) - || MediaTypeUtil.IsMatch(textJson, responseContentType.MediaType) - || MediaTypeUtil.IsMatch(problemJson, responseContentType.MediaType); + var mediaType = responseContentType.MediaType; + return mediaType != null && allowMediaTypes.Contains(mediaType); } /// /// 设置强类型模型结果值 /// /// 上下文 - /// + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("Trimming", "IL3050:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public override async Task SetResultAsync(ApiResponseContext context) { var resultType = context.ActionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/Attributes/ReturnAttributes/NoneReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/NoneReturnAttribute.cs index e2178144..4c09b080 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/NoneReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/NoneReturnAttribute.cs @@ -37,8 +37,7 @@ protected override bool CanUseDefaultValue(HttpResponseMessage responseMessage) return true; } - return responseMessage.IsSuccessStatusCode - && responseMessage.Content.Headers.ContentLength == 0; + return responseMessage.IsSuccessStatusCode && responseMessage.Content.Headers.ContentLength == 0; } } } diff --git a/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs index df1469e9..7d15df78 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs @@ -5,14 +5,14 @@ namespace WebApiClientCore.Attributes { /// /// 表示原始类型的结果特性 - /// 支持结果类型为string、byte[]、Stream和HttpResponseMessage + /// 支持结果类型为 string、byte[]、Stream 和 HttpResponseMessage /// public sealed class RawReturnAttribute : SpecialReturnAttribute { /// - /// 获取或设置是否确保响应的http状态码通过IsSuccessStatusCode验证 - /// 当值为true时,请求可能会引发HttpStatusFailureException - /// 默认为true + /// 获取或设置是否确保响应的 http 状态码通过IsSuccessStatusCode验证 + /// 当值为 true 时,请求可能会引发HttpStatusFailureException + /// 默认为 true /// public bool EnsureSuccessStatusCode { get; set; } = true; @@ -68,15 +68,15 @@ public async override Task SetResultAsync(ApiResponseContext context) } else if (dataType.IsRawString == true) { - context.Result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + context.Result = await response.Content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false); } else if (dataType.IsRawByteArray == true) { - context.Result = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + context.Result = await response.Content.ReadAsByteArrayAsync(context.RequestAborted).ConfigureAwait(false); } else if (dataType.IsRawStream == true) { - context.Result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + context.Result = await response.Content.ReadAsStreamAsync(context.RequestAborted).ConfigureAwait(false); } } } diff --git a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs index 8444c822..ed0f31a9 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs @@ -1,19 +1,18 @@ -using System.Net.Http.Headers; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Threading.Tasks; using WebApiClientCore.HttpContents; -using WebApiClientCore.Internals; namespace WebApiClientCore.Attributes { /// - /// 表示xml内容的结果特性 + /// 表示 xml 内容的结果特性 /// public class XmlReturnAttribute : ApiReturnAttribute { - /// - /// text/xml - /// - private static readonly string textXml = "text/xml"; + private static readonly HashSet allowMediaTypes = new([XmlContent.MediaType, "text/xml"], StringComparer.OrdinalIgnoreCase); /// /// xml内容的结果特性 @@ -34,14 +33,14 @@ public XmlReturnAttribute(double acceptQuality) /// /// 指示响应的ContentType与AcceptContentType是否匹配 - /// 返回false则调用下一个ApiReturnAttribute来处理响应结果 + /// 返回 false 则调用下一个ApiReturnAttribute来处理响应结果 /// /// 响应的ContentType /// protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseContentType) { - return base.IsMatchAcceptContentType(responseContentType) - || MediaTypeUtil.IsMatch(textXml, responseContentType.MediaType); + var mediaType = responseContentType.MediaType; + return mediaType != null && allowMediaTypes.Contains(mediaType); } /// @@ -49,6 +48,7 @@ protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseCo /// /// 上下文 /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public override async Task SetResultAsync(ApiResponseContext context) { var resultType = context.ActionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/BuildinExtensions/CollectionExtensions.cs b/WebApiClientCore/BuildinExtensions/CollectionExtensions.cs index cb77302b..04caaa95 100644 --- a/WebApiClientCore/BuildinExtensions/CollectionExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/CollectionExtensions.cs @@ -58,11 +58,7 @@ private static IEnumerable CollectAs(this IEnumerable keyVal /// public static IReadOnlyList ToReadOnlyList(this IEnumerable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - return source.ToList().AsReadOnly(); + return source == null ? throw new ArgumentNullException(nameof(source)) : (IReadOnlyList)source.ToList().AsReadOnly(); } } } diff --git a/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs b/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs index 42154ea1..0a906b2f 100644 --- a/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs @@ -1,6 +1,6 @@ -using System; +#if NETSTANDARD2_1 +using System; using System.Buffers; -using System.Diagnostics; using System.Text; namespace WebApiClientCore @@ -11,39 +11,174 @@ namespace WebApiClientCore static class EncodingExtensions { /// - /// 转换编码 + /// The maximum number of input elements after which we'll begin to chunk the input. /// - /// - /// 目标编码 - /// 源内容 - /// 目标写入器 - public static void Convert(this Encoding srcEncoding, Encoding dstEncoding, ReadOnlySpan buffer, IBufferWriter writer) + /// + /// The reason for this chunking is that the existing Encoding / Encoder / Decoder APIs + /// like GetByteCount / GetCharCount will throw if an integer overflow occurs. Since + /// we may be working with large inputs in these extension methods, we don't want to + /// risk running into this issue. While it's technically possible even for 1 million + /// input elements to result in an overflow condition, such a scenario is unrealistic, + /// so we won't worry about it. + /// + private const int MaxInputElementsPerIteration = 1 * 1024 * 1024; + + /// + /// Encodes the specified to s using the specified + /// and writes the result to . + /// + /// The which represents how the data in should be encoded. + /// The to encode to s. + /// The buffer to which the encoded bytes will be written. + /// Thrown if contains data that cannot be encoded and is configured + /// to throw an exception when such data is seen. + public static long GetBytes(this Encoding encoding, ReadOnlySpan chars, IBufferWriter writer) { - var decoder = srcEncoding.GetDecoder(); - var charCount = decoder.GetCharCount(buffer, false); - var charArray = charCount > 1024 ? ArrayPool.Shared.Rent(charCount) : null; - var chars = charArray == null ? stackalloc char[charCount] : charArray.AsSpan().Slice(0, charCount); + if (chars.Length <= MaxInputElementsPerIteration) + { + // The input span is small enough where we can one-shot this. + + int byteCount = encoding.GetByteCount(chars); + Span scratchBuffer = writer.GetSpan(byteCount); + + int actualBytesWritten = encoding.GetBytes(chars, scratchBuffer); - try + writer.Advance(actualBytesWritten); + return actualBytesWritten; + } + else { - decoder.Convert(buffer, chars, true, out _, out var charsUsed, out _); - Debug.Assert(charCount == charsUsed); + // Allocate a stateful Encoder instance and chunk this. - var encoder = dstEncoding.GetEncoder(); - var byteCount = encoder.GetByteCount(chars, false); - var bytes = writer.GetSpan(byteCount); + Convert(encoding.GetEncoder(), chars, writer, flush: true, out long totalBytesWritten, out _); + return totalBytesWritten; + } + } - encoder.Convert(chars, bytes, true, out _, out var byteUsed, out _); - Debug.Assert(byteCount == byteUsed); - writer.Advance(byteUsed); + /// + /// Decodes the specified to s using the specified + /// and writes the result to . + /// + /// The which represents how the data in should be decoded. + /// The whose bytes should be decoded. + /// The buffer to which the decoded chars will be written. + /// The number of chars written to . + /// Thrown if contains data that cannot be decoded and is configured + /// to throw an exception when such data is seen. + public static long GetChars(this Encoding encoding, ReadOnlySpan bytes, IBufferWriter writer) + { + if (bytes.Length <= MaxInputElementsPerIteration) + { + // The input span is small enough where we can one-shot this. + + int charCount = encoding.GetCharCount(bytes); + Span scratchBuffer = writer.GetSpan(charCount); + + int actualCharsWritten = encoding.GetChars(bytes, scratchBuffer); + + writer.Advance(actualCharsWritten); + return actualCharsWritten; } - finally + else { - if (charArray != null) - { - ArrayPool.Shared.Return(charArray); - } + // Allocate a stateful Decoder instance and chunk this. + + Convert(encoding.GetDecoder(), bytes, writer, flush: true, out long totalCharsWritten, out _); + return totalCharsWritten; } } + + /// + /// Converts a to bytes using and writes the result to . + /// + /// The instance which can convert s to s. + /// A sequence of characters to encode. + /// The buffer to which the encoded bytes will be written. + /// to indicate no further data is to be converted; otherwise . + /// When this method returns, contains the count of s which were written to . + /// + /// When this method returns, contains if contains no partial internal state; otherwise, . + /// If is , this will always be set to when the method returns. + /// + /// Thrown if contains data that cannot be encoded and is configured + /// to throw an exception when such data is seen. + public static void Convert(this Encoder encoder, ReadOnlySpan chars, IBufferWriter writer, bool flush, out long bytesUsed, out bool completed) + { + // We need to perform at least one iteration of the loop since the encoder could have internal state. + + long totalBytesWritten = 0; + + do + { + // If our remaining input is very large, instead truncate it and tell the encoder + // that there'll be more data after this call. This truncation is only for the + // purposes of getting the required byte count. Since the writer may give us a span + // larger than what we asked for, we'll pass the entirety of the remaining data + // to the transcoding routine, since it may be able to make progress beyond what + // was initially computed for the truncated input data. + + int byteCountForThisSlice = (chars.Length <= MaxInputElementsPerIteration) + ? encoder.GetByteCount(chars, flush) + : encoder.GetByteCount(chars.Slice(0, MaxInputElementsPerIteration), flush: false /* this isn't the end of the data */); + + Span scratchBuffer = writer.GetSpan(byteCountForThisSlice); + + encoder.Convert(chars, scratchBuffer, flush, out int charsUsedJustNow, out int bytesWrittenJustNow, out completed); + + chars = chars.Slice(charsUsedJustNow); + writer.Advance(bytesWrittenJustNow); + totalBytesWritten += bytesWrittenJustNow; + } while (!chars.IsEmpty); + + bytesUsed = totalBytesWritten; + } + + + /// + /// Converts a to chars using and writes the result to . + /// + /// The instance which can convert s to s. + /// A sequence of bytes to decode. + /// The buffer to which the decoded chars will be written. + /// to indicate no further data is to be converted; otherwise . + /// When this method returns, contains the count of s which were written to . + /// + /// When this method returns, contains if contains no partial internal state; otherwise, . + /// If is , this will always be set to when the method returns. + /// + /// Thrown if contains data that cannot be encoded and is configured + /// to throw an exception when such data is seen. + public static void Convert(this Decoder decoder, ReadOnlySpan bytes, IBufferWriter writer, bool flush, out long charsUsed, out bool completed) + { + // We need to perform at least one iteration of the loop since the decoder could have internal state. + + long totalCharsWritten = 0; + + do + { + // If our remaining input is very large, instead truncate it and tell the decoder + // that there'll be more data after this call. This truncation is only for the + // purposes of getting the required char count. Since the writer may give us a span + // larger than what we asked for, we'll pass the entirety of the remaining data + // to the transcoding routine, since it may be able to make progress beyond what + // was initially computed for the truncated input data. + + int charCountForThisSlice = (bytes.Length <= MaxInputElementsPerIteration) + ? decoder.GetCharCount(bytes, flush) + : decoder.GetCharCount(bytes.Slice(0, MaxInputElementsPerIteration), flush: false /* this isn't the end of the data */); + + Span scratchBuffer = writer.GetSpan(charCountForThisSlice); + + decoder.Convert(bytes, scratchBuffer, flush, out int bytesUsedJustNow, out int charsWrittenJustNow, out completed); + + bytes = bytes.Slice(bytesUsedJustNow); + writer.Advance(charsWrittenJustNow); + totalCharsWritten += charsWrittenJustNow; + } while (!bytes.IsEmpty); + + charsUsed = totalCharsWritten; + } } } + +#endif \ No newline at end of file diff --git a/WebApiClientCore/BuildinExtensions/HttpContentExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpContentExtensions.cs new file mode 100644 index 00000000..41cfa114 --- /dev/null +++ b/WebApiClientCore/BuildinExtensions/HttpContentExtensions.cs @@ -0,0 +1,34 @@ +#if NETSTANDARD2_1 +using System.IO; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace WebApiClientCore +{ + /// + /// HttpContent扩展 + /// + static class HttpContentExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task ReadAsByteArrayAsync(this HttpContent httpContent, CancellationToken _) + { + return httpContent.ReadAsByteArrayAsync(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task ReadAsStreamAsync(this HttpContent httpContent, CancellationToken _) + { + return httpContent.ReadAsStreamAsync(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task ReadAsStringAsync(this HttpContent httpContent, CancellationToken _) + { + return httpContent.ReadAsStringAsync(); + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs index d0939236..a96fc904 100644 --- a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs @@ -1,8 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; namespace WebApiClientCore { @@ -11,52 +8,63 @@ namespace WebApiClientCore /// static class HttpRequestHeaderExtensions { - /// - /// HttpRequestHeader的类型 - /// - private static readonly Type httpRequestHeaderType = typeof(HttpRequestHeader); - /// /// 请求头枚举和名称的缓存 /// - private static readonly Dictionary cache = []; - - /// - /// 请求头枚举到名称的转换 - /// - static HttpRequestHeaderExtensions() - { - foreach (var header in Enum.GetValues(httpRequestHeaderType).Cast()) - { - cache.Add(header, header.GetHeaderName()); - } - } - - /// - /// 返回枚举的DisplayName - /// - /// 请求头枚举 - /// - private static string GetHeaderName(this HttpRequestHeader header) + private static readonly Dictionary cache = new() { - return httpRequestHeaderType - .GetField(header.ToString())? - .GetCustomAttribute()? - .Name ?? header.ToString(); - } + [HttpRequestHeader.CacheControl] = "Cache-Control", + [HttpRequestHeader.Connection] = "Connection", + [HttpRequestHeader.Date] = "Date", + [HttpRequestHeader.KeepAlive] = "Keep-Alive", + [HttpRequestHeader.Pragma] = "Pragma", + [HttpRequestHeader.Trailer] = "Trailer", + [HttpRequestHeader.TransferEncoding] = "Transfer-Encoding", + [HttpRequestHeader.Upgrade] = "Upgrade", + [HttpRequestHeader.Via] = "Via", + [HttpRequestHeader.Warning] = "Warning", + [HttpRequestHeader.Allow] = "Allow", + [HttpRequestHeader.ContentLength] = "Content-Length", + [HttpRequestHeader.ContentType] = "Content-Type", + [HttpRequestHeader.ContentEncoding] = "Content-Encoding", + [HttpRequestHeader.ContentLanguage] = "Content-Language", + [HttpRequestHeader.ContentLocation] = "Content-Location", + [HttpRequestHeader.ContentMd5] = "Content-MD5", + [HttpRequestHeader.ContentRange] = "Content-Range", + [HttpRequestHeader.Expires] = "Expires", + [HttpRequestHeader.LastModified] = "Last-Modified", + [HttpRequestHeader.Accept] = "Accept", + [HttpRequestHeader.AcceptCharset] = "Accept-Charset", + [HttpRequestHeader.AcceptEncoding] = "Accept-Encoding", + [HttpRequestHeader.AcceptLanguage] = "Accept-Language", + [HttpRequestHeader.Authorization] = "Authorization", + [HttpRequestHeader.Cookie] = "Cookie", + [HttpRequestHeader.Expect] = "Expect", + [HttpRequestHeader.From] = "From", + [HttpRequestHeader.Host] = "Host", + [HttpRequestHeader.IfMatch] = "If-Match", + [HttpRequestHeader.IfModifiedSince] = "If-Modified-Since", + [HttpRequestHeader.IfNoneMatch] = "If-None-Match", + [HttpRequestHeader.IfRange] = "If-Range", + [HttpRequestHeader.IfUnmodifiedSince] = "If-Unmodified-Since", + [HttpRequestHeader.MaxForwards] = "Max-Forwards", + [HttpRequestHeader.ProxyAuthorization] = "Proxy-Authorization", + [HttpRequestHeader.Referer] = "Referer", + [HttpRequestHeader.Range] = "Range", + [HttpRequestHeader.Te] = "TE", + [HttpRequestHeader.Translate] = "Translate", + [HttpRequestHeader.UserAgent] = "User-Agent" + }; /// - /// 转换为header名 + /// 转换为 header 名 /// /// 请求头枚举 + /// /// public static string ToHeaderName(this HttpRequestHeader header) { - if (cache.TryGetValue(header, out var name)) - { - return name; - } - return header.ToString(); + return cache.TryGetValue(header, out var name) ? name : throw new ArgumentOutOfRangeException(nameof(header)); } } -} \ No newline at end of file +} diff --git a/WebApiClientCore/BuildinExtensions/StringExtensions.cs b/WebApiClientCore/BuildinExtensions/StringExtensions.cs index 071e65b9..09199c47 100644 --- a/WebApiClientCore/BuildinExtensions/StringExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/StringExtensions.cs @@ -27,7 +27,7 @@ public static string Format(this string str, params object?[] args) /// 替换的新值 /// 是否替换成功 /// - public static string RepaceIgnoreCase(this string source, ReadOnlySpan oldValue, ReadOnlySpan newValue, out bool replaced) + public static string ReplaceIgnoreCase(this string source, ReadOnlySpan oldValue, ReadOnlySpan newValue, out bool replaced) { replaced = false; if (string.IsNullOrEmpty(source) || oldValue.IsEmpty) @@ -41,9 +41,9 @@ public static string RepaceIgnoreCase(this string source, ReadOnlySpan old while ((index = FindIndexIgnoreCase(sourceSpan, oldValue)) > -1) { - builder.Append(sourceSpan.Slice(0, index)); + builder.Append(sourceSpan[..index]); builder.Append(newValue); - sourceSpan = sourceSpan.Slice(index + oldValue.Length); + sourceSpan = sourceSpan[(index + oldValue.Length)..]; replaced = true; } diff --git a/WebApiClientCore/BuildinExtensions/TypeExtensions.cs b/WebApiClientCore/BuildinExtensions/TypeExtensions.cs index 830eccd6..cb2f4831 100644 --- a/WebApiClientCore/BuildinExtensions/TypeExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/TypeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -61,14 +62,13 @@ public static bool IsInheritFrom(this Type type) /// 参数值 /// /// - public static T CreateInstance(this Type type, params object?[] args) + public static T CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + this Type type, + params object?[] args) { var instance = Activator.CreateInstance(type, args); - if (instance == null) - { - throw new TypeInstanceCreateException(type); - } - return (T)instance; + return instance == null ? throw new TypeInstanceCreateException(type) : (T)instance; } /// @@ -76,7 +76,9 @@ public static T CreateInstance(this Type type, params object?[] args) /// /// 接口类型 /// - public static Attribute[] GetInterfaceCustomAttributes(this Type interfaceType) + public static Attribute[] GetInterfaceCustomAttributes( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + this Type interfaceType) { return interfaceType .GetInterfaces() diff --git a/WebApiClientCore/CodeAnalysis/DynamicDependencyAttribute.cs b/WebApiClientCore/CodeAnalysis/DynamicDependencyAttribute.cs new file mode 100644 index 00000000..82b6f844 --- /dev/null +++ b/WebApiClientCore/CodeAnalysis/DynamicDependencyAttribute.cs @@ -0,0 +1,32 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// 表示动态依赖属性 + /// + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = true, Inherited = false)] + sealed class DynamicDependencyAttribute : Attribute + { + /// + /// 获取或设置动态访问的成员类型 + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// 获取或设置依赖的类型 + /// + public Type? Type { get; } + + /// + /// 初始化 类的新实例 + /// + /// 动态访问的成员类型 + /// 依赖的类型 + public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type) + { + this.MemberTypes = memberTypes; + this.Type = type; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/WebApiClientCore/CodeAnalysis/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 00000000..54bcd902 --- /dev/null +++ b/WebApiClientCore/CodeAnalysis/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,85 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies the types of dynamically accessed members. + /// + enum DynamicallyAccessedMemberTypes + { + /// + /// All member types are dynamically accessed. + /// + All = -1, + + /// + /// No member types are dynamically accessed. + /// + None = 0, + + /// + /// Public parameterless constructors are dynamically accessed. + /// + PublicParameterlessConstructor = 1, + + /// + /// Public constructors are dynamically accessed. + /// + PublicConstructors = 3, + + /// + /// Non-public constructors are dynamically accessed. + /// + NonPublicConstructors = 4, + + /// + /// Public methods are dynamically accessed. + /// + PublicMethods = 8, + + /// + /// Non-public methods are dynamically accessed. + /// + NonPublicMethods = 16, + + /// + /// Public fields are dynamically accessed. + /// + PublicFields = 32, + + /// + /// Non-public fields are dynamically accessed. + /// + NonPublicFields = 64, + + /// + /// Public nested types are dynamically accessed. + /// + PublicNestedTypes = 128, + + /// + /// Non-public nested types are dynamically accessed. + /// + NonPublicNestedTypes = 256, + + /// + /// Public properties are dynamically accessed. + /// + PublicProperties = 512, + + /// + /// Non-public properties are dynamically accessed. + /// + NonPublicProperties = 1024, + + /// + /// Public events are dynamically accessed. + /// + PublicEvents = 2048, + + /// + /// Non-public events are dynamically accessed. + /// + NonPublicEvents = 4096, + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/WebApiClientCore/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs new file mode 100644 index 00000000..30a744b9 --- /dev/null +++ b/WebApiClientCore/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the members accessed dynamically at runtime are considered used. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false)] + sealed class DynamicallyAccessedMembersAttribute : Attribute + { + /// + /// Gets the types of dynamically accessed members. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// Initializes a new instance of the class with the specified member types. + /// + /// The types of dynamically accessed members. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + this.MemberTypes = memberTypes; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/CodeAnalysis/RequiresDynamicCodeAttribute.cs b/WebApiClientCore/CodeAnalysis/RequiresDynamicCodeAttribute.cs new file mode 100644 index 00000000..e94faa6d --- /dev/null +++ b/WebApiClientCore/CodeAnalysis/RequiresDynamicCodeAttribute.cs @@ -0,0 +1,25 @@ +#if NETSTANDARD2_1 || NET5_0 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the attributed class, constructor, or method requires dynamic code. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + sealed class RequiresDynamicCodeAttribute : Attribute + { + /// + /// Gets the message associated with the requirement for dynamic code. + /// + public string Message { get; } + + /// + /// Initializes a new instance of the class with the specified message. + /// + /// The message associated with the requirement for dynamic code. + public RequiresDynamicCodeAttribute(string message) + { + this.Message = message; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs b/WebApiClientCore/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 00000000..ee93f9d3 --- /dev/null +++ b/WebApiClientCore/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,22 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// 获取或设置对于未引用代码的要求的消息。 + /// + public string Message { get; } + + /// + /// 初始化 类的新实例。 + /// + /// 对于未引用代码的要求的消息。 + public RequiresUnreferencedCodeAttribute(string message) + { + this.Message = message; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/WebApiClientCore/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs new file mode 100644 index 00000000..caa21e39 --- /dev/null +++ b/WebApiClientCore/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs @@ -0,0 +1,52 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// 表示一个用于取消对代码分析器规则的警告的特性 + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + sealed class UnconditionalSuppressMessageAttribute : Attribute + { + /// + /// 获取或设置警告的类别 + /// + public string Category { get; } + + /// + /// 获取或设置要取消的检查标识符 + /// + public string CheckId { get; } + + /// + /// 获取或设置取消警告的理由 + /// + public string? Justification { get; set; } + + /// + /// 获取或设置消息的标识符 + /// + public string? MessageId { get; set; } + + /// + /// 获取或设置取消警告的范围 + /// + public string? Scope { get; set; } + + /// + /// 获取或设置取消警告的目标 + /// + public string? Target { get; set; } + + /// + /// 初始化 类的新实例 + /// + /// 警告的类别 + /// 要取消的检查标识符 + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + this.Category = category; + this.CheckId = checkId; + } + } +} +#endif \ No newline at end of file diff --git a/WebApiClientCore/DependencyInjection/HttpApiConfigureExtensions.cs b/WebApiClientCore/DependencyInjection/HttpApiConfigureExtensions.cs index 4ffcbfef..c16e3432 100644 --- a/WebApiClientCore/DependencyInjection/HttpApiConfigureExtensions.cs +++ b/WebApiClientCore/DependencyInjection/HttpApiConfigureExtensions.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection public static class HttpApiConfigureExtensions { /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// @@ -23,7 +23,7 @@ public static IServiceCollection ConfigureHttpApi(this IServiceCollect } /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// @@ -35,7 +35,7 @@ public static IServiceCollection ConfigureHttpApi(this IServiceCollect } /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// @@ -49,7 +49,7 @@ public static IServiceCollection ConfigureHttpApi(this IServiceCollect /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// 接口类型 @@ -61,7 +61,7 @@ public static IServiceCollection ConfigureHttpApi(this IServiceCollection servic } /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// 接口类型 @@ -73,7 +73,7 @@ public static IServiceCollection ConfigureHttpApi(this IServiceCollection servic } /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// 接口类型 @@ -85,7 +85,7 @@ public static IServiceCollection ConfigureHttpApi(this IServiceCollection servic } /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// @@ -96,7 +96,7 @@ private static OptionsBuilder AddHttpApiOptions(this I } /// - /// 为接口配置HttpApiOptions + /// 为接口配置 /// /// /// 接口类型 diff --git a/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs b/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs index 2ccbc129..db17f87f 100644 --- a/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs +++ b/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using WebApiClientCore; using WebApiClientCore.Implementations; @@ -18,11 +19,8 @@ public static class HttpApiExtensions /// /// /// - public static IHttpClientBuilder AddHttpApi< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi>(this IServiceCollection services) where THttpApi : class + public static IHttpClientBuilder AddHttpApi<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services) where THttpApi : class { var name = HttpApi.GetName(typeof(THttpApi)); @@ -45,11 +43,9 @@ public static IHttpClientBuilder AddHttpApi< /// /// 配置选项 /// - public static IHttpClientBuilder AddHttpApi< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi>(this IServiceCollection services, Action configureOptions) where THttpApi : class + public static IHttpClientBuilder AddHttpApi<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Action configureOptions) where THttpApi : class { return services .AddHttpApi() @@ -63,11 +59,9 @@ public static IHttpClientBuilder AddHttpApi< /// /// 配置选项 /// - public static IHttpClientBuilder AddHttpApi< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi>(this IServiceCollection services, Action configureOptions) where THttpApi : class + public static IHttpClientBuilder AddHttpApi<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Action configureOptions) where THttpApi : class { return services .AddHttpApi() @@ -82,7 +76,9 @@ public static IHttpClientBuilder AddHttpApi< /// 接口类型 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Type httpApiType) + public static IHttpClientBuilder AddHttpApi( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType) { if (httpApiType == null) { @@ -105,7 +101,10 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Ty /// 配置选项 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Type httpApiType, Action configureOptions) + public static IHttpClientBuilder AddHttpApi( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + Action configureOptions) { return services .AddHttpApi(httpApiType) @@ -120,7 +119,10 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Ty /// 配置选项 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Type httpApiType, Action configureOptions) + public static IHttpClientBuilder AddHttpApi( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + Action configureOptions) { return services .AddHttpApi(httpApiType) @@ -133,11 +135,7 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Ty /// 表示THttpApi提供者 /// /// - private class HttpApiProvider< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi> + private class HttpApiProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> { private readonly IHttpClientFactory httpClientFactory; private readonly IOptionsMonitor httpApiOptionsMonitor; @@ -190,11 +188,9 @@ private abstract class HttpApiAdder /// /// 接口类型 /// - -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors, typeof(HttpApiAdderOf<>))] -#endif - public static HttpApiAdder Create(Type httpApiType) + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(HttpApiAdderOf<>))] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] + public static HttpApiAdder Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType) { var adderType = typeof(HttpApiAdderOf<>).MakeGenericType(httpApiType); return adderType.CreateInstance(); @@ -204,11 +200,8 @@ public static HttpApiAdder Create(Type httpApiType) /// 表示HttpApi服务添加者 /// /// - private class HttpApiAdderOf< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi> : HttpApiAdder where THttpApi : class + private class HttpApiAdderOf<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : HttpApiAdder where THttpApi : class { /// /// 添加HttpApi代理类到服务 diff --git a/WebApiClientCore/DependencyInjection/NamedHttpApiExtensions.cs b/WebApiClientCore/DependencyInjection/NamedHttpApiExtensions.cs index e0af97ea..7e48d542 100644 --- a/WebApiClientCore/DependencyInjection/NamedHttpApiExtensions.cs +++ b/WebApiClientCore/DependencyInjection/NamedHttpApiExtensions.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection public static class NamedHttpApiExtensions { /// - /// 注册http接口类型的别名 + /// 注册 http 接口类型的别名 /// /// /// 接口别名 @@ -26,7 +26,7 @@ internal static void NamedHttpApiType(this IServiceCollection services, string n } /// - /// 获取builder关联的HttpApi类型 + /// 获取 builder 关联的HttpApi类型 /// /// /// diff --git a/WebApiClientCore/DependencyInjection/WebApiClientBuilderExtensions.cs b/WebApiClientCore/DependencyInjection/WebApiClientBuilderExtensions.cs index 7c608328..737c8f63 100644 --- a/WebApiClientCore/DependencyInjection/WebApiClientBuilderExtensions.cs +++ b/WebApiClientCore/DependencyInjection/WebApiClientBuilderExtensions.cs @@ -18,10 +18,10 @@ public static class WebApiClientBuilderExtensions /// 添加WebApiClient全局默认配置 /// /// - /// • 尝试使用DefaultHttpApiActivator,使用SourceGenerator生成的代理类或Emit动态创建代理类来创建代理实例 - /// • 尝试使用DefaultApiActionDescriptorProvider,缺省参数特性声明时为参数应用PathQueryAttribute - /// • 尝试使用DefaultResponseCacheProvider,在内存中缓存响应结果 - /// • 尝试使用DefaultApiActionInvokerProvider + /// • 尝试使用,注册为 + /// • 尝试使用,注册为 + /// • 尝试使用,注册为 + /// • 尝试使用,注册为 /// /// /// @@ -39,6 +39,29 @@ public static IWebApiClientBuilder AddWebApiClient(this IServiceCollection servi return new WebApiClientBuilder(services); } + + /// + /// 使用 替换 的实现 + /// + /// IWebApiClientBuilder 实例 + /// 返回 IWebApiClientBuilder 实例 + public static IWebApiClientBuilder UseILEmitHttpApiActivator(this IWebApiClientBuilder builder) + { + builder.Services.RemoveAll(typeof(IHttpApiActivator<>)).AddSingleton(typeof(IHttpApiActivator<>), typeof(ILEmitHttpApiActivator<>)); + return builder; + } + + /// + /// 使用 替换 的实现 + /// + /// + /// + public static IWebApiClientBuilder UseSourceGeneratorHttpApiActivator(this IWebApiClientBuilder builder) + { + builder.Services.RemoveAll(typeof(IHttpApiActivator<>)).AddSingleton(typeof(IHttpApiActivator<>), typeof(SourceGeneratorHttpApiActivator<>)); + return builder; + } + /// /// 当非GET或HEAD请求的缺省参数特性声明时 /// 为复杂参数类型的参数应用JsonContentAttribute @@ -52,7 +75,7 @@ public static IWebApiClientBuilder UseJsonFirstApiActionDescriptor(this IWebApiC } /// - /// 配置HttpApiOptions的默认值 + /// 配置的默认值 /// /// /// 配置选项 @@ -64,7 +87,7 @@ public static IWebApiClientBuilder ConfigureHttpApi(this IWebApiClientBuilder bu } /// - /// 配置HttpApiOptions的默认值 + /// 配置的默认值 /// /// /// 配置选项 @@ -76,7 +99,7 @@ public static IWebApiClientBuilder ConfigureHttpApi(this IWebApiClientBuilder bu } /// - /// 配置HttpApiOptions的默认值 + /// 配置的默认值 /// /// /// 配置 diff --git a/WebApiClientCore/Exceptions/ApiReturnNotSupportedException.cs b/WebApiClientCore/Exceptions/ApiReturnNotSupportedException.cs new file mode 100644 index 00000000..6fffd274 --- /dev/null +++ b/WebApiClientCore/Exceptions/ApiReturnNotSupportedException.cs @@ -0,0 +1,17 @@ +namespace WebApiClientCore.Exceptions +{ + /// + /// + /// + public class ApiReturnNotSupportedException : ApiReturnNotSupportedExteption + { + /// + /// + /// + /// + public ApiReturnNotSupportedException(ApiResponseContext context) + : base(context) + { + } + } +} diff --git a/WebApiClientCore/Exceptions/ApiReturnNotSupportedExteption.cs b/WebApiClientCore/Exceptions/ApiReturnNotSupportedExteption.cs index 142b1663..47745c82 100644 --- a/WebApiClientCore/Exceptions/ApiReturnNotSupportedExteption.cs +++ b/WebApiClientCore/Exceptions/ApiReturnNotSupportedExteption.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Net; namespace WebApiClientCore.Exceptions @@ -6,6 +7,7 @@ namespace WebApiClientCore.Exceptions /// /// 表示接口不支持处理响应消息的异常 /// + [EditorBrowsable(EditorBrowsableState.Never)] public class ApiReturnNotSupportedExteption : ApiException, IStatusCodeException { /// diff --git a/WebApiClientCore/HttpApi.cs b/WebApiClientCore/HttpApi.cs index a680e252..54c65d33 100644 --- a/WebApiClientCore/HttpApi.cs +++ b/WebApiClientCore/HttpApi.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -38,7 +39,8 @@ public static string GetName(Type? httpApiType, bool includeNamespace) var builder = new ValueStringBuilder(stackalloc char[256]); if (includeNamespace == true) { - builder.Append(httpApiType.Namespace).Append("."); + builder.Append(httpApiType.Namespace); + builder.Append("."); } GetName(httpApiType, ref builder); @@ -89,8 +91,9 @@ private static void GetName(Type type, ref ValueStringBuilder builder) /// 接口类型 /// /// - /// - public static MethodInfo[] FindApiMethods(Type httpApiType) + /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "已使用DynamicallyAccessedMembers.All关联接口的父接口成员")] + public static MethodInfo[] FindApiMethods([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType) { if (httpApiType.IsInterface == false) { diff --git a/WebApiClientCore/HttpApiProxyMethodAttribute.cs b/WebApiClientCore/HttpApiProxyMethodAttribute.cs index 17287bfc..998226d6 100644 --- a/WebApiClientCore/HttpApiProxyMethodAttribute.cs +++ b/WebApiClientCore/HttpApiProxyMethodAttribute.cs @@ -13,22 +13,48 @@ public sealed class HttpApiProxyMethodAttribute : Attribute /// /// 获取索引值 /// - public int Index { get; } + public int Index { get; } = -1; /// /// 获取名称 /// - public string Name { get; } + public string Name { get; } = string.Empty; + + /// + /// 获取方法所在的声明类型 + /// + public Type? DeclaringType { get; } + + /// + /// 方法的索引特性 + /// + /// 索引值 + [Obsolete("仅为了兼容v2.0.8的SourceGenerator语法")] + public HttpApiProxyMethodAttribute(int index) + { + } /// /// 方法的索引特性 /// - /// 索引值,确保连续且不重复 + /// 索引值 /// 方法的名称 + [Obsolete("仅为了兼容v2.0.9的SourceGenerator语法")] public HttpApiProxyMethodAttribute(int index, string name) + { + } + + /// + /// 方法的索引特性 + /// + /// 索引值 + /// 方法的名称 + /// 法所在的声明类型 + public HttpApiProxyMethodAttribute(int index, string name, Type? declaringType) { this.Index = index; this.Name = name; + this.DeclaringType = declaringType; } } } diff --git a/WebApiClientCore/HttpContents/BufferContent.cs b/WebApiClientCore/HttpContents/BufferContent.cs index 8c92e5bf..edc183ba 100644 --- a/WebApiClientCore/HttpContents/BufferContent.cs +++ b/WebApiClientCore/HttpContents/BufferContent.cs @@ -10,7 +10,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示utf8的BufferContent + /// 表示缓冲的 Http 内容 /// public class BufferContent : HttpContent, IBufferWriter { @@ -20,11 +20,31 @@ public class BufferContent : HttpContent, IBufferWriter private readonly RecyclableBufferWriter bufferWriter = new(); /// - /// utf8的BufferContent + /// 获取已数入的数据 + /// + protected ArraySegment WrittenSegment => this.bufferWriter.WrittenSegment; + + /// + /// 缓冲的 Http 内容 /// public BufferContent(string mediaType) + : this(new MediaTypeHeaderValue(mediaType)) + { + } + + /// + /// 缓冲的 Http 内容 + /// + public BufferContent(MediaTypeHeaderValue mediaType) + { + this.Headers.ContentType = mediaType; + } + + /// + /// 缓冲的 Http 内容 + /// + protected BufferContent() { - this.Headers.ContentType = new MediaTypeHeaderValue(mediaType); } /// @@ -78,6 +98,14 @@ public void Write(Span buffer) this.bufferWriter.Write(buffer); } + /// + /// 清除数据 + /// + public void Clear() + { + this.bufferWriter.Clear(); + } + /// /// 创建只读流 /// diff --git a/WebApiClientCore/HttpContents/FormContent.cs b/WebApiClientCore/HttpContents/FormContent.cs index c3836616..9d80a130 100644 --- a/WebApiClientCore/HttpContents/FormContent.cs +++ b/WebApiClientCore/HttpContents/FormContent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -13,49 +14,54 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示键值对表单内容 + /// 表示 application/x-www-form-urlencoded 表单内容 /// public class FormContent : HttpContent { + private const string mediaType = "application/x-www-form-urlencoded"; + private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new(mediaType); + /// /// buffer写入器 /// private readonly RecyclableBufferWriter bufferWriter = new(); /// - /// 默认的http编码 + /// 默认的 http 编码 /// private static readonly Encoding httpEncoding = Encoding.GetEncoding(28591); /// /// 获取对应的ContentType /// - public static string MediaType => "application/x-www-form-urlencoded"; + public static string MediaType => mediaType; /// - /// 键值对表单内容 + /// application/x-www-form-urlencoded 表单内容 /// public FormContent() { - this.Headers.ContentType = new MediaTypeHeaderValue(MediaType); + this.Headers.ContentType = mediaTypeHeaderValue; } /// - /// 键值对表单内容 + /// application/x-www-form-urlencoded 表单内容 /// /// 键值对 public FormContent(IEnumerable keyValues) + : this() { this.AddFormField(keyValues); - this.Headers.ContentType = new MediaTypeHeaderValue(MediaType); } /// - /// 键值对表单内容 + /// application/x-www-form-urlencoded 表单内容 /// /// 模型对象值 /// 序列化选项 + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public FormContent(object? value, KeyValueSerializerOptions? options) { if (value != null) @@ -105,15 +111,18 @@ public void AddFormField(IEnumerable keyValues) /// 添加已编码的原始内容表单 /// /// 表单内容 - public void AddForm(string? encodedForm) + public void AddForm(ReadOnlySpan encodedForm) { - if (encodedForm == null) + this.EnsureNotBuffered(); + + if (encodedForm.Length > 0) { - return; + if (this.bufferWriter.WrittenCount > 0) + { + this.bufferWriter.Write((byte)'&'); + } + httpEncoding.GetBytes(encodedForm, this.bufferWriter); } - - var formBytes = httpEncoding.GetBytes(encodedForm); - this.AddForm(formBytes); } /// @@ -124,16 +133,14 @@ public void AddForm(ReadOnlySpan encodedForm) { this.EnsureNotBuffered(); - if (encodedForm.IsEmpty == true) - { - return; - } - - if (this.bufferWriter.WrittenCount > 0) + if (encodedForm.Length > 0) { - this.bufferWriter.Write((byte)'&'); + if (this.bufferWriter.WrittenCount > 0) + { + this.bufferWriter.Write((byte)'&'); + } + this.bufferWriter.Write(encodedForm); } - this.bufferWriter.Write(encodedForm); } /// @@ -184,7 +191,7 @@ protected override void Dispose(bool disposing) /// 从HttpContent转换得到 /// /// httpContent实例 - /// 是否释放httpContent + /// 是否释放 httpContent /// public static async Task ParseAsync(HttpContent? httpContent, bool disposeHttpContent = true) { diff --git a/WebApiClientCore/HttpContents/FormDataContent.cs b/WebApiClientCore/HttpContents/FormDataContent.cs index e4d5b678..b67972e5 100644 --- a/WebApiClientCore/HttpContents/FormDataContent.cs +++ b/WebApiClientCore/HttpContents/FormDataContent.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示form-data表单 + /// 表示 multipart/form-data 表单 /// public class FormDataContent : MultipartContent, ICustomHttpContentConvertable { @@ -20,7 +20,7 @@ public class FormDataContent : MultipartContent, ICustomHttpContentConvertable public static string MediaType => "multipart/form-data"; /// - /// form-data表单 + /// multipart/form-data 表单 /// public FormDataContent() : this(Guid.NewGuid().ToString()) @@ -28,7 +28,7 @@ public FormDataContent() } /// - /// form-data表单 + /// multipart/form-data 表单 /// /// 分隔符 public FormDataContent(string boundary) @@ -41,7 +41,7 @@ public FormDataContent(string boundary) } /// - /// 添加httpContent + /// 添加 httpContent /// /// public override void Add(HttpContent content) @@ -51,7 +51,7 @@ public override void Add(HttpContent content) } /// - /// 转换为自定义HttpConent的HttpContent + /// 转换为自定义内容的HttpContent /// /// public HttpContent ToCustomHttpContext() @@ -59,9 +59,9 @@ public HttpContent ToCustomHttpContext() var customHttpContent = new FormDataContent(this.boundary); foreach (var httpContent in this) { - if (httpContent is ICustomHttpContentConvertable convertable) + if (httpContent is ICustomHttpContentConvertable conversable) { - customHttpContent.Add(convertable.ToCustomHttpContext()); + customHttpContent.Add(conversable.ToCustomHttpContext()); } else { diff --git a/WebApiClientCore/HttpContents/FormDataFileContent.cs b/WebApiClientCore/HttpContents/FormDataFileContent.cs index 51297051..4bd1f25d 100644 --- a/WebApiClientCore/HttpContents/FormDataFileContent.cs +++ b/WebApiClientCore/HttpContents/FormDataFileContent.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示form-data文件内容 + /// 表示 multipart/form-data 文件内容 /// public class FormDataFileContent : StreamContent, ICustomHttpContentConvertable { @@ -15,7 +15,7 @@ public class FormDataFileContent : StreamContent, ICustomHttpContentConvertable public static string OctetStream => "application/octet-stream"; /// - /// form-data文件内容 + /// multipart/form-data 文件内容 /// /// 文件流 /// 名称 @@ -58,7 +58,7 @@ private class EllipsisContent : ByteArrayContent /// /// 省略号内容 /// - private static readonly byte[] content = new[] { (byte)'.', (byte)'.', (byte)'.' }; + private static readonly byte[] content = "..."u8.ToArray(); /// /// 省略内容的文件请求内容 diff --git a/WebApiClientCore/HttpContents/FormDataTextContent.cs b/WebApiClientCore/HttpContents/FormDataTextContent.cs index c62c7bb2..7e2313d5 100644 --- a/WebApiClientCore/HttpContents/FormDataTextContent.cs +++ b/WebApiClientCore/HttpContents/FormDataTextContent.cs @@ -4,12 +4,12 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示form-data文本内容 + /// 表示 multipart/form-data 文本内容 /// public class FormDataTextContent : StringContent { /// - /// form-data文本内容 + /// multipart/form-data 文本内容 /// /// 键值对 public FormDataTextContent(KeyValue keyValue) @@ -18,7 +18,7 @@ public FormDataTextContent(KeyValue keyValue) } /// - /// form-data文本内容 + /// multipart/form-data 文本内容 /// /// 名称 /// 文本 diff --git a/WebApiClientCore/HttpContents/ICustomHttpContentConvertable.cs b/WebApiClientCore/HttpContents/ICustomHttpContentConvertable.cs index c3767d8f..6baabb5b 100644 --- a/WebApiClientCore/HttpContents/ICustomHttpContentConvertable.cs +++ b/WebApiClientCore/HttpContents/ICustomHttpContentConvertable.cs @@ -3,7 +3,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 定义支持转换为自定义HttpConent的接口 + /// 定义支持转换为自定义HttpContent的接口 /// interface ICustomHttpContentConvertable { diff --git a/WebApiClientCore/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index 83d5c6cc..1ec14e37 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -1,33 +1,39 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; +using System.Text; using System.Text.Json; -using WebApiClientCore.Internals; using WebApiClientCore.Serialization; namespace WebApiClientCore.HttpContents { /// - /// 表示uft8的json内容 + /// 表示 uft8 的 json 内容 /// public class JsonContent : BufferContent { + private const string mediaType = "application/json"; + private static readonly MediaTypeHeaderValue defaultMediaType = new(mediaType); + /// /// 获取对应的ContentType /// - public static string MediaType => "application/json"; + public static string MediaType => mediaType; /// - /// uft8的json内容 + /// uft8 的 json 内容 /// public JsonContent() - : base(MediaType) + : base(defaultMediaType) { } /// - /// uft8的json内容 + /// uft8 的 json 内容 /// /// 对象值 /// json序列化选项 + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) : this(value, jsonSerializerOptions, null) { @@ -39,21 +45,30 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) /// 对象值 /// json序列化选项 /// 编码 + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions, Encoding? encoding) - : base(MediaType) { + JsonBufferSerializer.Serialize(this, value, jsonSerializerOptions); + if (encoding == null || Encoding.UTF8.Equals(encoding)) { - JsonBufferSerializer.Serialize(this, value, jsonSerializerOptions); + this.Headers.ContentType = defaultMediaType; } else { - using var utf8Writer = new RecyclableBufferWriter(); - JsonBufferSerializer.Serialize(utf8Writer, value, jsonSerializerOptions); - - Encoding.UTF8.Convert(encoding, utf8Writer.WrittenSpan, this); - this.Headers.ContentType!.CharSet = encoding.WebName; + this.Headers.ContentType = new MediaTypeHeaderValue(mediaType) { CharSet = encoding.WebName }; + this.ConvertEncoding(encoding); } } + + private void ConvertEncoding(Encoding encoding) + { + var utf8Json = this.WrittenSegment; + var encodingBuffer = Encoding.Convert(Encoding.UTF8, encoding, utf8Json.Array!, utf8Json.Offset, utf8Json.Count); + + this.Clear(); + this.Write(encodingBuffer); + } } } diff --git a/WebApiClientCore/HttpContents/JsonPatchContent.cs b/WebApiClientCore/HttpContents/JsonPatchContent.cs index 5b787bff..1d15cd1f 100644 --- a/WebApiClientCore/HttpContents/JsonPatchContent.cs +++ b/WebApiClientCore/HttpContents/JsonPatchContent.cs @@ -1,36 +1,43 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Text.Json; using WebApiClientCore.Serialization; namespace WebApiClientCore.HttpContents { /// - /// 表示utf8的JsonPatch内容 + /// 表示 utf8 的JsonPatch内容 /// public class JsonPatchContent : BufferContent { + private const string mediaType = "application/json-patch+json"; + private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new(mediaType); + /// /// 获取对应的ContentType /// - public static string MediaType => "application/json-patch+json"; + public static string MediaType => mediaType; /// /// utf8的JsonPatch内容 /// public JsonPatchContent() - : base(MediaType) + : base(mediaTypeHeaderValue) { } /// /// utf8的JsonPatch内容 /// - /// patch操作项 + /// patch操作项 /// json序列化选项 - public JsonPatchContent(IEnumerable oprations, JsonSerializerOptions? jsonSerializerOptions) - : base(MediaType) + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + public JsonPatchContent(IEnumerable operations, JsonSerializerOptions? jsonSerializerOptions) + : base(mediaTypeHeaderValue) { - JsonBufferSerializer.Serialize(this, oprations, jsonSerializerOptions); + JsonBufferSerializer.Serialize(this, operations, jsonSerializerOptions); } } } diff --git a/WebApiClientCore/HttpContents/XmlContent.cs b/WebApiClientCore/HttpContents/XmlContent.cs index 0ba4c35f..665b0d97 100644 --- a/WebApiClientCore/HttpContents/XmlContent.cs +++ b/WebApiClientCore/HttpContents/XmlContent.cs @@ -1,13 +1,16 @@ -using System.Net.Http; +using System; +using System.Net.Http.Headers; using System.Text; namespace WebApiClientCore.HttpContents { /// - /// 表示xml内容 + /// 表示 xml 内容 /// - public class XmlContent : StringContent + public class XmlContent : BufferContent { + private static readonly MediaTypeHeaderValue defaultMediaType = new(MediaType) { CharSet = Encoding.UTF8.WebName }; + /// /// 获取对应的ContentType /// @@ -18,9 +21,12 @@ public class XmlContent : StringContent /// /// xml内容 /// 编码 - public XmlContent(string? xml, Encoding encoding) - : base(xml ?? string.Empty, encoding, MediaType) + public XmlContent(ReadOnlySpan xml, Encoding encoding) { + encoding.GetBytes(xml, this); + this.Headers.ContentType = encoding == Encoding.UTF8 + ? defaultMediaType : + new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; } } -} +} \ No newline at end of file diff --git a/WebApiClientCore/HttpMessageHandlers/AuthorizationHandler.cs b/WebApiClientCore/HttpMessageHandlers/AuthorizationHandler.cs index 6dbc43f3..a399d4c4 100644 --- a/WebApiClientCore/HttpMessageHandlers/AuthorizationHandler.cs +++ b/WebApiClientCore/HttpMessageHandlers/AuthorizationHandler.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.HttpMessageHandlers { /// - /// 表示授权应用的抽象http消息处理程序 + /// 表示授权应用的抽象 http 消息处理程序 /// public abstract class AuthorizationHandler : DelegatingHandler { @@ -53,7 +53,7 @@ private async Task SendAsync(SetReason reason, HttpRequestM /// /// 返回响应是否为未授权状态 - /// 反回true则触发重试请求 + /// 反回 true 则触发重试请求 /// /// 响应消息 protected virtual Task IsUnauthorizedAsync(HttpResponseMessage response) diff --git a/WebApiClientCore/HttpMessageHandlers/CookieAuthorizationHandler.cs b/WebApiClientCore/HttpMessageHandlers/CookieAuthorizationHandler.cs index 345233eb..3c6ba640 100644 --- a/WebApiClientCore/HttpMessageHandlers/CookieAuthorizationHandler.cs +++ b/WebApiClientCore/HttpMessageHandlers/CookieAuthorizationHandler.cs @@ -6,7 +6,7 @@ namespace WebApiClientCore.HttpMessageHandlers { /// - /// 表示cookie授权验证的抽象http消息处理程序 + /// 表示 cookie 授权验证的抽象 http 消息处理程序 /// public abstract class CookieAuthorizationHandler : AuthorizationHandler { diff --git a/WebApiClientCore/HttpPath.cs b/WebApiClientCore/HttpPath.cs index c053f350..36b2233a 100644 --- a/WebApiClientCore/HttpPath.cs +++ b/WebApiClientCore/HttpPath.cs @@ -3,14 +3,14 @@ namespace WebApiClientCore { /// - /// 表示http路径 + /// 表示 http 路径 /// public abstract class HttpPath { /// /// 合成Uri /// - /// 基础uri + /// 基础 uri /// public abstract Uri? MakeUri(Uri? baseUri); diff --git a/WebApiClientCore/HttpRequestHeader.cs b/WebApiClientCore/HttpRequestHeader.cs index 98dbcc58..052264db 100644 --- a/WebApiClientCore/HttpRequestHeader.cs +++ b/WebApiClientCore/HttpRequestHeader.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace WebApiClientCore { @@ -32,7 +32,7 @@ public enum HttpRequestHeader : byte KeepAlive = 3, /// - /// Pragma 标头,指定可应用于请求/响应链上的任何代理的特定于实现的指令 + /// Pragma 标头,指定可应用于请求/响应链上的任何代理的特定于实现的指令 /// [Display(Name = "Pragma")] Pragma = 4, @@ -116,7 +116,7 @@ public enum HttpRequestHeader : byte ContentRange = 17, /// - /// Expires 标头,指定日期和时间,在此之后伴随的正文数据应视为陈旧的 + /// Expires 标头,指定日期和时间,在此之后伴随的正文数据应视为陈旧的 /// [Display(Name = "Expires")] Expires = 18, diff --git a/WebApiClientCore/IChunkedable.cs b/WebApiClientCore/IChunkedable.cs new file mode 100644 index 00000000..84601fa8 --- /dev/null +++ b/WebApiClientCore/IChunkedable.cs @@ -0,0 +1,13 @@ +namespace WebApiClientCore +{ + /// + /// 是否允许chunked传输 + /// + interface IChunkedable + { + /// + /// 获取或设置是否允许 chunked 传输 + /// + bool AllowChunked { get; set; } + } +} diff --git a/WebApiClientCore/Implementations/ApiRequestExecuter.cs b/WebApiClientCore/Implementations/ApiRequestExecutor.cs similarity index 67% rename from WebApiClientCore/Implementations/ApiRequestExecuter.cs rename to WebApiClientCore/Implementations/ApiRequestExecutor.cs index 2385fb81..792838ca 100644 --- a/WebApiClientCore/Implementations/ApiRequestExecuter.cs +++ b/WebApiClientCore/Implementations/ApiRequestExecutor.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace WebApiClientCore.Implementations @@ -6,7 +9,7 @@ namespace WebApiClientCore.Implementations /// /// 请求上下文执行器 /// - static class ApiRequestExecuter + static class ApiRequestExecutor { /// /// 执行上下文 @@ -16,7 +19,9 @@ static class ApiRequestExecuter public static async Task ExecuteAsync(ApiRequestContext request) { await HandleRequestAsync(request).ConfigureAwait(false); - var response = await ApiRequestSender.SendAsync(request).ConfigureAwait(false); + using var requestAbortedLinker = new CancellationTokenLinker(request.HttpContext.CancellationTokens); + + var response = await ApiRequestSender.SendAsync(request, requestAbortedLinker.Token).ConfigureAwait(false); await HandleResponseAsync(response).ConfigureAwait(false); return response; } @@ -117,5 +122,58 @@ private static async Task HandleResponseAsync(ApiResponseContext context) await filter.OnResponseAsync(context).ConfigureAwait(false); } } + + /// + /// 表示CancellationToken链接器 + /// + private readonly struct CancellationTokenLinker : IDisposable + { + /// + /// 链接产生的 tokenSource + /// + private readonly CancellationTokenSource? tokenSource; + + /// + /// 获取 token + /// + public CancellationToken Token { get; } + + /// + /// CancellationToken链接器 + /// + /// + public CancellationTokenLinker(IList tokenList) + { + if (IsNoneCancellationToken(tokenList)) + { + this.tokenSource = null; + this.Token = CancellationToken.None; + } + else + { + this.tokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenList.ToArray()); + this.Token = this.tokenSource.Token; + } + } + + /// + /// 是否为None的CancellationToken + /// + /// + /// + private static bool IsNoneCancellationToken(IList tokenList) + { + var count = tokenList.Count; + return (count == 0) || (count == 1 && tokenList[0] == CancellationToken.None); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + this.tokenSource?.Dispose(); + } + } } } diff --git a/WebApiClientCore/Implementations/ApiRequestSender.cs b/WebApiClientCore/Implementations/ApiRequestSender.cs index f0a26766..1a36420e 100644 --- a/WebApiClientCore/Implementations/ApiRequestSender.cs +++ b/WebApiClientCore/Implementations/ApiRequestSender.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -9,17 +7,18 @@ namespace WebApiClientCore.Implementations { /// - /// 提供http请求 + /// 提供 http 请求 /// static class ApiRequestSender { /// - /// 发送http请求 + /// 发送 http 请求 /// /// + /// /// /// - public static async Task SendAsync(ApiRequestContext context) + public static async Task SendAsync(ApiRequestContext context, CancellationToken requestAborted) { if (context.HttpContext.RequestMessage.RequestUri == null) { @@ -28,24 +27,25 @@ public static async Task SendAsync(ApiRequestContext context try { - await SendCoreAsync(context).ConfigureAwait(false); - return new ApiResponseContext(context); + await SendCoreAsync(context, requestAborted).ConfigureAwait(false); + return new ApiResponseContext(context, requestAborted); } catch (Exception ex) { - return new ApiResponseContext(context) { Exception = ex }; + return new ApiResponseContext(context, requestAborted) { Exception = ex }; } } /// - /// 发送http请求 + /// 发送 http 请求 /// /// + /// /// /// - private static async Task SendCoreAsync(ApiRequestContext context) + private static async Task SendCoreAsync(ApiRequestContext context, CancellationToken requestAborted) { - var actionCache = await context.GetCaheAsync().ConfigureAwait(false); + var actionCache = await context.GetCacheAsync().ConfigureAwait(false); if (actionCache != null && actionCache.Value != null) { context.HttpContext.ResponseMessage = actionCache.Value; @@ -54,22 +54,46 @@ private static async Task SendCoreAsync(ApiRequestContext context) { var client = context.HttpContext.HttpClient; var request = context.HttpContext.RequestMessage; - var completionOption = context.GetCompletionOption(); - - using var tokenLinker = new CancellationTokenLinker(context.HttpContext.CancellationTokens); - var response = await client.SendAsync(request, completionOption, tokenLinker.Token).ConfigureAwait(false); + var completionOption = GetCompletionOption(context); + var response = await client.SendAsync(request, completionOption, requestAborted).ConfigureAwait(false); context.HttpContext.ResponseMessage = response; await context.SetCacheAsync(actionCache?.Key, response).ConfigureAwait(false); } } + + /// + /// 返回请求使用的HttpCompletionOption + /// + /// + /// + private static HttpCompletionOption GetCompletionOption(ApiRequestContext context) + { + if (context.HttpContext.CompletionOption != null) + { + return context.HttpContext.CompletionOption.Value; + } + + if (context.ActionDescriptor.Return.DataType.IsRawType) + { + return HttpCompletionOption.ResponseHeadersRead; + } + + if (context.ActionDescriptor.FilterAttributes.Count == 0 && context.HttpContext.HttpApiOptions.GlobalFilters.Count == 0) + { + return HttpCompletionOption.ResponseHeadersRead; + } + + return HttpCompletionOption.ResponseContentRead; + } + /// /// 获取响应的缓存 /// /// /// - private static async Task GetCaheAsync(this ApiRequestContext context) + private static async Task GetCacheAsync(this ApiRequestContext context) { var attribute = context.ActionDescriptor.CacheAttribute; if (attribute == null) @@ -177,68 +201,5 @@ public ActionCache(string key, HttpResponseMessage? value) this.Value = value; } } - - /// - /// 表示CancellationToken链接器 - /// - private readonly struct CancellationTokenLinker : IDisposable - { - /// - /// 链接产生的tokenSource - /// - private readonly CancellationTokenSource? tokenSource; - - /// - /// 获取token - /// - public CancellationToken Token { get; } - - /// - /// CancellationToken链接器 - /// - /// - public CancellationTokenLinker(IList tokenList) - { - if (IsNoneCancellationToken(tokenList) == true) - { - this.tokenSource = null; - this.Token = CancellationToken.None; - } - else - { - this.tokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenList.ToArray()); - this.Token = this.tokenSource.Token; - } - } - - /// - /// 是否为None的CancellationToken - /// - /// - /// - private static bool IsNoneCancellationToken(IList tokenList) - { - if (tokenList.Count == 0) - { - return true; - } - if (tokenList.Count == 1 && tokenList[0] == CancellationToken.None) - { - return true; - } - return false; - } - - /// - /// 释放资源 - /// - public void Dispose() - { - if (this.tokenSource != null) - { - this.tokenSource.Dispose(); - } - } - } } } diff --git a/WebApiClientCore/Implementations/DataValidator.cs b/WebApiClientCore/Implementations/DataValidator.cs index 26ccd72b..b453f81d 100644 --- a/WebApiClientCore/Implementations/DataValidator.cs +++ b/WebApiClientCore/Implementations/DataValidator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace WebApiClientCore.Implementations @@ -24,6 +25,7 @@ static class DataValidator /// 参数值 /// 是否验证属性值 /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许 parameterValue 属性被裁剪")] public static void ValidateParameter(ApiParameterDescriptor parameter, object? parameterValue, bool validateProperty) { var name = parameter.Name; @@ -44,6 +46,7 @@ public static void ValidateParameter(ApiParameterDescriptor parameter, object? p /// /// 结果值 /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许 value 属性被裁剪")] public static void ValidateReturnValue(object? value) { if (value != null && IsNeedValidateProperty(value) == true) @@ -58,6 +61,7 @@ public static void ValidateReturnValue(object? value) /// /// 实例 /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "允许 instance 属性被裁剪")] private static bool IsNeedValidateProperty(object instance) { var type = instance.GetType(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs index 9ba4fc5d..80364bb9 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -17,6 +18,8 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// 获取所在接口类型 /// 这个值不一定是声明方法的接口类型 /// + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public override Type InterfaceType { get; protected set; } /// @@ -58,13 +61,14 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// /// 获取自定义数据存储的字典 /// - public override ConcurrentDictionary Properties { get; protected set; } + public override ConcurrentDictionary Properties { get; protected set; } /// /// 请求Api描述 /// for test only /// - /// 接口的方法 + /// 接口的方法 + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "允许DeclaringType被裁剪")] internal DefaultApiActionDescriptor(MethodInfo method) : this(method, method.DeclaringType!) { @@ -75,7 +79,9 @@ internal DefaultApiActionDescriptor(MethodInfo method) /// /// 接口的方法 /// 接口类型 - public DefaultApiActionDescriptor(MethodInfo method, Type interfaceType) + public DefaultApiActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { var methodAttributes = method.GetCustomAttributes().ToArray(); var interfaceAttributes = interfaceType.GetInterfaceCustomAttributes(); @@ -103,7 +109,7 @@ public DefaultApiActionDescriptor(MethodInfo method, Type interfaceType) this.Attributes = actionAttributes; this.CacheAttribute = methodAttributes.OfType().FirstOrDefault(); this.FilterAttributes = filterAttributes; - this.Properties = new ConcurrentDictionary(); + this.Properties = new ConcurrentDictionary(); this.Return = new DefaultApiReturnDescriptor(method.ReturnType, methodAttributes, interfaceAttributes); this.Parameters = method.GetParameters().Select(p => new DefaultApiParameterDescriptor(p)).ToReadOnlyList(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs index f6bd2779..aad7d307 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace WebApiClientCore.Implementations @@ -13,7 +14,9 @@ public class DefaultApiActionDescriptorProvider : IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - public virtual ApiActionDescriptor CreateActionDescriptor(MethodInfo method, Type interfaceType) + public virtual ApiActionDescriptor CreateActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { return new DefaultApiActionDescriptor(method, interfaceType); } diff --git a/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs b/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs index ac3ff6a4..20c59f79 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs @@ -92,7 +92,7 @@ public virtual async Task InvokeAsync(HttpClientContext context, object private static async Task InvokeAsync(ApiRequestContext request) { #nullable disable - var response = await ApiRequestExecuter.ExecuteAsync(request).ConfigureAwait(false); + var response = await ApiRequestExecutor.ExecuteAsync(request).ConfigureAwait(false); if (response.ResultStatus == ResultStatus.HasResult) { return (TResult)response.Result; @@ -103,7 +103,7 @@ private static async Task InvokeAsync(ApiRequestContext request) ExceptionDispatchInfo.Capture(response.Exception).Throw(); } - throw new ApiReturnNotSupportedExteption(response); + throw new ApiReturnNotSupportedException(response); #nullable enable } diff --git a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs index 706f7b1d..d324f555 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; namespace WebApiClientCore.Implementations { @@ -17,9 +18,9 @@ public ApiActionInvoker CreateActionInvoker(ApiActionDescriptor actionDescriptor var actionInvoker = this.CreateDefaultActionInvoker(actionDescriptor); if (actionDescriptor.Return.ReturnType.IsInheritFrom() == false) { - if (actionInvoker is IITaskReturnConvertable convertable) + if (actionInvoker is IITaskReturnConvertable conversable) { - actionInvoker = convertable.ToITaskReturnActionInvoker(); + actionInvoker = conversable.ToITaskReturnActionInvoker(); } } return actionInvoker; @@ -29,10 +30,9 @@ public ApiActionInvoker CreateActionInvoker(ApiActionDescriptor actionDescriptor /// 创建DefaultApiActionInvoker类型或其子类型的实例 /// /// Action描述 - /// -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors, typeof(DefaultApiActionInvoker<>))] -#endif + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(DefaultApiActionInvoker<>))] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] protected virtual ApiActionInvoker CreateDefaultActionInvoker(ApiActionDescriptor actionDescriptor) { var resultType = actionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/Implementations/DefaultApiDataTypeDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiDataTypeDescriptor.cs index dd21c9b7..f960f105 100644 --- a/WebApiClientCore/Implementations/DefaultApiDataTypeDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiDataTypeDescriptor.cs @@ -27,7 +27,7 @@ public class DefaultApiDataTypeDescriptor : ApiDataTypeDescriptor public override bool IsRawStream { get; protected set; } /// - /// 获取是否为原始类型的byte[] + /// 获取是否为原始类型的 byte[] /// public override bool IsRawByteArray { get; protected set; } diff --git a/WebApiClientCore/Implementations/DefaultApiParameterDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiParameterDescriptor.cs index 3c83efae..b64505a9 100644 --- a/WebApiClientCore/Implementations/DefaultApiParameterDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiParameterDescriptor.cs @@ -67,9 +67,9 @@ public DefaultApiParameterDescriptor(ParameterInfo parameter) /// 请求Api的参数描述 /// /// 参数信息 - /// 缺省特性时使用的默认特性 + /// 缺省特性时使用的默认特性 /// - public DefaultApiParameterDescriptor(ParameterInfo parameter, IApiParameterAttribute defaultAtribute) + public DefaultApiParameterDescriptor(ParameterInfo parameter, IApiParameterAttribute defaultAttribute) { if (parameter == null) { @@ -92,7 +92,7 @@ public DefaultApiParameterDescriptor(ParameterInfo parameter, IApiParameterAttri var attributes = this.GetAttributes(parameter, parameterAttributes).ToArray(); if (attributes.Length == 0) { - this.Attributes = new[] { defaultAtribute }.ToReadOnlyList(); + this.Attributes = new[] { defaultAttribute }.ToReadOnlyList(); } else { diff --git a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs index bfc03112..e71652f9 100644 --- a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Reflection; @@ -34,6 +35,7 @@ public class DefaultApiReturnDescriptor : ApiReturnDescriptor /// for test only /// /// 方法信息 + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "允许DeclaringType被裁剪")] internal DefaultApiReturnDescriptor(MethodInfo method) : this(method, method.DeclaringType!) { @@ -44,7 +46,9 @@ internal DefaultApiReturnDescriptor(MethodInfo method) /// /// 方法信息 /// 接口类型 - public DefaultApiReturnDescriptor(MethodInfo method, Type interfaceType) + public DefaultApiReturnDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) : this(method.ReturnType, method.GetCustomAttributes(), interfaceType.GetInterfaceCustomAttributes()) { } @@ -84,7 +88,7 @@ public DefaultApiReturnDescriptor(Type returnType, IEnumerable method /// private static IEnumerable GetDefaultAttributes(ApiDataTypeDescriptor dataType) { - const double acceptQuality = 0.001; + const double acceptQuality = 0.1; if (dataType.IsRawType == true) { yield return new RawReturnAttribute(acceptQuality); @@ -92,8 +96,8 @@ private static IEnumerable GetDefaultAttributes(ApiDataType else { yield return new NoneReturnAttribute(acceptQuality); - yield return new JsonReturnAttribute(acceptQuality); - yield return new XmlReturnAttribute(acceptQuality); + yield return new JsonReturnAttribute(acceptQuality) { EnsureMatchAcceptContentType = true }; + yield return new XmlReturnAttribute(acceptQuality) { EnsureMatchAcceptContentType = true }; } } diff --git a/WebApiClientCore/Implementations/DefaultDataCollection.cs b/WebApiClientCore/Implementations/DefaultDataCollection.cs index fba71e1a..93704275 100644 --- a/WebApiClientCore/Implementations/DefaultDataCollection.cs +++ b/WebApiClientCore/Implementations/DefaultDataCollection.cs @@ -38,7 +38,7 @@ public void Set(object key, object? value) } /// - /// 返回是否包含指定的key + /// 返回是否包含指定的 key /// /// 键 /// @@ -49,7 +49,7 @@ public bool ContainsKey(object key) /// /// 读取指定的键并尝试转换为目标类型 - /// 失败则返回目标类型的default值 + /// 失败则返回目标类型的 default 值 /// /// /// 键 diff --git a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs index 516df648..ac777da4 100644 --- a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs @@ -1,4 +1,7 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using WebApiClientCore.Exceptions; namespace WebApiClientCore.Implementations { @@ -8,14 +11,10 @@ namespace WebApiClientCore.Implementations /// 不支持则回退使用EmitHttpApiActivator /// /// - public class DefaultHttpApiActivator< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi> : IHttpApiActivator + public class DefaultHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { - private readonly Lazy> emitHttpApiActivatorLazy; - private readonly SourceGeneratorHttpApiActivator? sourceGeneratorHttpApiActivator; + private readonly IHttpApiActivator httpApiActivator; /// /// 默认的THttpApi的实例创建器 @@ -24,13 +23,21 @@ public class DefaultHttpApiActivator< /// /// /// + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "ILEmitHttpApiActivator使用之前已经使用RuntimeFeature.IsDynamicCodeCompiled来判断")] public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { if (SourceGeneratorHttpApiActivator.IsSupported) { - this.sourceGeneratorHttpApiActivator = new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); + this.httpApiActivator = new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); + } + else if (RuntimeFeature.IsDynamicCodeCompiled) + { + this.httpApiActivator = new ILEmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); + } + else + { + throw new ProxyTypeCreateException(typeof(HttpApi)); } - this.emitHttpApiActivatorLazy = new Lazy>(() => new EmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider), isThreadSafe: true); } /// @@ -40,9 +47,7 @@ public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorP /// public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) { - return this.sourceGeneratorHttpApiActivator == null - ? this.emitHttpApiActivatorLazy.Value.CreateInstance(apiInterceptor) - : this.sourceGeneratorHttpApiActivator.CreateInstance(apiInterceptor); + return this.httpApiActivator.CreateInstance(apiInterceptor); } } } \ No newline at end of file diff --git a/WebApiClientCore/Implementations/HttpApiActivator.cs b/WebApiClientCore/Implementations/HttpApiActivator.cs index 3dc782e2..ed10cefa 100644 --- a/WebApiClientCore/Implementations/HttpApiActivator.cs +++ b/WebApiClientCore/Implementations/HttpApiActivator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -9,7 +10,8 @@ namespace WebApiClientCore.Implementations /// /// [Obsolete("该类型存在构造器调用虚方法的设计失误,不建议再使用", error: false)] - public abstract class HttpApiActivator : IHttpApiActivator + public abstract class HttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { /// /// 接口的所有方法执行器 diff --git a/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs index 1826277b..4789048c 100644 --- a/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs +++ b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs @@ -13,7 +13,7 @@ namespace WebApiClientCore.Implementations { /// - /// 表示httpApi的请求消息 + /// 表示HttpApi的请求消息 /// sealed class HttpApiRequestMessageImpl : HttpApiRequestMessage { @@ -25,7 +25,7 @@ sealed class HttpApiRequestMessageImpl : HttpApiRequestMessage /// /// 请求头的默认UserAgent /// - private readonly static ProductInfoHeaderValue defaultUserAgent = new(assemblyName.Name ?? "WebApiClientCore", assemblyName.Version?.ToString()); + private readonly static ProductInfoHeaderValue defaultUserAgent = new(assemblyName.Name ?? nameof(WebApiClientCore), assemblyName.Version?.ToString()); /// /// httpApi的请求消息 @@ -38,7 +38,7 @@ public HttpApiRequestMessageImpl() /// /// httpApi的请求消息 /// - /// 请求uri + /// 请求 uri /// 请求头是否包含默认的UserAgent public HttpApiRequestMessageImpl(Uri? requestUri, bool useDefaultUserAgent) { @@ -50,7 +50,7 @@ public HttpApiRequestMessageImpl(Uri? requestUri, bool useDefaultUserAgent) } /// - /// 返回使用uri值合成的请求URL + /// 返回使用 uri 值合成的请求URL /// /// uri值 /// @@ -71,7 +71,7 @@ public override Uri MakeRequestUri(Uri uri) } /// - /// 创建uri + /// 创建 uri /// /// /// @@ -92,8 +92,8 @@ private static Uri CreateUriByRelative(Uri? baseUri, Uri relative) } /// - /// 创建uri - /// 参数值的uri是绝对uir,且只有根路径 + /// 创建 uri + /// 参数值的 uri 是绝对 uir,且只有根路径 /// /// /// @@ -110,7 +110,7 @@ private static Uri CreateUriByAbsolute(Uri absolute, Uri? uri) } /// - /// 返回相对uri + /// 返回相对 uri /// /// uri /// @@ -121,14 +121,9 @@ private static string GetRelativeUri(Uri uri) return uri.OriginalString; } - var path = uri.OriginalString.AsSpan().Slice(uri.Scheme.Length + 3); + var path = uri.OriginalString.AsSpan()[(uri.Scheme.Length + 3)..]; var index = path.IndexOf('/'); - if (index < 0) - { - return "/"; - } - - return path.Slice(index).ToString(); + return index < 0 ? "/" : path[index..].ToString(); } /// @@ -157,7 +152,7 @@ public override void AddUrlQuery(string key, string? value) /// /// 添加字段到已有的Content - /// 要求content-type为application/x-www-form-urlencoded + /// 要求 content-type 为 application/x-www-form-urlencoded /// /// 键值对 /// @@ -173,7 +168,7 @@ public override async Task AddFormFieldAsync(IEnumerable keyValues) /// /// 添加文本内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 键值对 /// @@ -182,7 +177,7 @@ public override void AddFormDataText(IEnumerable keyValues) { this.EnsureMediaTypeEqual(FormDataContent.MediaType); - if (!(this.Content is MultipartContent httpContent)) + if (this.Content is not MultipartContent httpContent) { httpContent = new FormDataContent(); } @@ -198,7 +193,7 @@ public override void AddFormDataText(IEnumerable keyValues) /// /// 添加文件内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 文件流 /// 名称 @@ -209,18 +204,18 @@ public override void AddFormDataFile(Stream stream, string name, string? fileNam { this.EnsureMediaTypeEqual(FormDataContent.MediaType); - if (!(this.Content is MultipartContent httpContent)) + if (this.Content is not MultipartContent httpContent) { httpContent = new FormDataContent(); + this.Content = httpContent; } var fileContent = new FormDataFileContent(stream, name, fileName, contentType); httpContent.Add(fileContent); - this.Content = httpContent; } /// - /// 确保前后的mediaType一致 + /// 确保前后的 mediaType 一致 /// /// 新的MediaType /// diff --git a/WebApiClientCore/Implementations/EmitHttpApiActivator.cs b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs similarity index 87% rename from WebApiClientCore/Implementations/EmitHttpApiActivator.cs rename to WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs index 622cfc9a..3b709187 100644 --- a/WebApiClientCore/Implementations/EmitHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -8,26 +9,25 @@ namespace WebApiClientCore.Implementations { /// - /// 运行时使用Emit动态创建THttpApi的代理类和代理类实例 + /// 运行时使用ILEmit动态创建THttpApi的代理类和代理类实例 /// /// - public class EmitHttpApiActivator< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi> : IHttpApiActivator + public sealed class ILEmitHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { private readonly ApiActionInvoker[] actionInvokers; private readonly Func activator; /// - /// 运行时使用Emit动态创建THttpApi的代理类和代理类实例 + /// 运行时使用ILEmit动态创建THttpApi的代理类和代理类实例 /// /// /// /// /// - public EmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "proxyType是运行时生成的")] + [RequiresDynamicCode("Calls System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)")] + public ILEmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var apiMethods = HttpApi.FindApiMethods(typeof(THttpApi)); @@ -36,7 +36,7 @@ public EmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProv .Select(actionInvokerProvider.CreateActionInvoker) .ToArray(); - var proxyType = BuildProxyType(typeof(THttpApi), apiMethods); + var proxyType = BuildProxyType(apiMethods); this.activator = LambdaUtil.CreateCtorFunc(proxyType); } @@ -66,15 +66,16 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// /// 创建IHttpApi代理类的类型 - /// - /// 接口类型 + /// /// 接口的方法 /// /// /// - private static Type BuildProxyType(Type interfaceType, MethodInfo[] apiMethods) + [RequiresDynamicCode("Calls System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)")] + private static Type BuildProxyType(MethodInfo[] apiMethods) { - // 接口的实现在动态程序集里,所以接口必须为public修饰才可以创建代理类并实现此接口 + // 接口的实现在动态程序集里,所以接口必须为 public 修饰才可以创建代理类并实现此接口 + var interfaceType = typeof(THttpApi); if (interfaceType.IsVisible == false) { var message = Resx.required_PublicInterface.Format(interfaceType); @@ -195,10 +196,10 @@ private static void BuildMethods(TypeBuilder builder, MethodInfo[] actionMethods iL.Emit(OpCodes.Stelem_Ref); } - // 加载arguments参数 + // 加载 arguments 参数 iL.Emit(OpCodes.Ldloc, arguments); - // Intercep(actionInvoker, arguments) + // Intercept(actionInvoker, arguments) iL.Emit(OpCodes.Callvirt, interceptMethod); if (actionMethod.ReturnType == typeof(void)) diff --git a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs index aeb09f0f..4d02f02a 100644 --- a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs +++ b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; @@ -23,7 +24,9 @@ public class JsonFirstApiActionDescriptor : DefaultApiActionDescriptor /// /// /// - public JsonFirstApiActionDescriptor(MethodInfo method, Type interfaceType) + public JsonFirstApiActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) : base(method, interfaceType) { var defineGetHead = this.Attributes.Any(a => this.IsGetHeadAttribute(a)); @@ -67,7 +70,7 @@ protected virtual bool IsGetHeadAttribute(IApiActionAttribute apiActionAttribute /// 是否为简单类型 /// 这些类型缺省特性时仍然使用PathQueryAttribute /// - /// 真实类型,非nullable + /// 真实类型,非 nullable /// protected virtual bool IsSimpleType(Type realType) { diff --git a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs index 96bb7f10..1980394c 100644 --- a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs +++ b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace WebApiClientCore.Implementations @@ -14,7 +15,9 @@ public class JsonFirstApiActionDescriptorProvider : IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - public ApiActionDescriptor CreateActionDescriptor(MethodInfo method, Type interfaceType) + public ApiActionDescriptor CreateActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { return new JsonFirstApiActionDescriptor(method, interfaceType); } diff --git a/WebApiClientCore/Implementations/MultiplableComparer.cs b/WebApiClientCore/Implementations/MultiplableComparer.cs index 1291c90d..96ee1803 100644 --- a/WebApiClientCore/Implementations/MultiplableComparer.cs +++ b/WebApiClientCore/Implementations/MultiplableComparer.cs @@ -25,7 +25,7 @@ public bool Equals(TAttributeMultiplable? x, TAttributeMultiplable? y) return false; } - // 如果其中一个不允许重复,返回true将y过滤 + // 如果其中一个不允许重复,返回 true 将y过滤 if (x.GetType() == y.GetType()) { return x.AllowMultiple == false || y.AllowMultiple == false; diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index 1901dd70..953b3b2d 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using WebApiClientCore.Exceptions; @@ -11,15 +14,12 @@ namespace WebApiClientCore.Implementations /// 通过查找类型代理类型创建实例 /// /// - public class SourceGeneratorHttpApiActivator< -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - THttpApi> : IHttpApiActivator + public sealed class SourceGeneratorHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { private readonly ApiActionInvoker[] actionInvokers; private readonly Func activator; - private static readonly Type? _proxyClassType = SourceGeneratorProxyClassType.Find(typeof(THttpApi)); + private static readonly Type? _proxyClassType = SourceGeneratorProxyClassFinder.Find(typeof(THttpApi)); /// /// 获取是否支持 @@ -34,6 +34,7 @@ public class SourceGeneratorHttpApiActivator< /// /// /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "类型 proxyClassType 已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] public SourceGeneratorHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var httpApiType = typeof(THttpApi); @@ -68,39 +69,46 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// 接口类型 /// 接口的实现类型 /// - private static MethodInfo[] FindApiMethods(Type httpApiType, Type proxyClassType) + private static IEnumerable FindApiMethods( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type proxyClassType) { - var apiMethods = HttpApi.FindApiMethods(httpApiType); - var classMethods = proxyClassType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); - - // 按照Index特征对apiMethods进行排序 - var query = from a in apiMethods.Select(item => new MethodFeature(item, isProxyMethod: false)) - join c in classMethods.Select(item => new MethodFeature(item, isProxyMethod: true)) - on a equals c - orderby c.Index - select a.Method; - - var methods = query.ToArray(); - if (apiMethods.Length != methods.Length) + var apiMethods = HttpApi.FindApiMethods(httpApiType) + .Select(item => new MethodFeature(item, isProxyMethod: false)) + .ToArray(); + + var classMethods = proxyClassType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Select(item => new MethodFeature(item, isProxyMethod: true)) + .Where(item => item.Index >= 0) + .ToArray(); + + if (apiMethods.Length != classMethods.Length) { - var missingMethod = apiMethods.Except(methods).FirstOrDefault(); - var message = $"{httpApiType}的代理类缺失方法{missingMethod}"; + var message = $"接口类型{httpApiType}的代理类{proxyClassType}和当前版本不兼容,请将{httpApiType.Assembly.GetName().Name}项目所依赖的WebApiClientCore更新到版本v{typeof(SourceGeneratorHttpApiActivator<>).Assembly.GetName().Version}"; throw new ProxyTypeException(httpApiType, message); } - return methods; + + // 按照 Index 特征对 apiMethods 进行排序 + return from a in apiMethods + join c in classMethods + on a equals c + orderby c.Index + select a.Method; } /// /// 表示MethodInfo的特征 /// + [DebuggerDisplay("[{Index,nq}] {declaringType.FullName,nq}.{name,nq}")] private sealed class MethodFeature : IEquatable { + private readonly string name; + private readonly Type? declaringType; + public MethodInfo Method { get; } public int Index { get; } - public string Name { get; } - /// /// MethodInfo的特征 /// @@ -116,8 +124,18 @@ public MethodFeature(MethodInfo method, bool isProxyMethod) attribute = method.GetCustomAttribute(); } - this.Index = attribute == null ? -1 : attribute.Index; - this.Name = attribute == null ? $"{method.DeclaringType?.FullName}.{method.Name}" : attribute.Name; + if (attribute == null) + { + this.Index = -1; + this.declaringType = method.DeclaringType; + this.name = method.Name; + } + else + { + this.Index = attribute.Index; + this.declaringType = attribute.DeclaringType; + this.name = attribute.Name; + } } /// @@ -127,7 +145,9 @@ public MethodFeature(MethodInfo method, bool isProxyMethod) /// public bool Equals(MethodFeature? other) { - if (other == null || this.Name != other.Name) + if (other == null || + this.name != other.name || + this.declaringType != other.declaringType) { return false; } @@ -157,7 +177,9 @@ public override bool Equals(object? obj) public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(this.Name); + + hashCode.Add(this.declaringType); + hashCode.Add(this.name); hashCode.Add(this.Method.ReturnType); foreach (var parameter in this.Method.GetParameters()) { diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs new file mode 100644 index 00000000..37e71adf --- /dev/null +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace WebApiClientCore.Implementations +{ + /// + /// 提供获取SourceGenerator生成的代理类型 + /// + static class SourceGeneratorProxyClassFinder + { + private static readonly object syncRoot = new(); + private static readonly HashSet assemblies = []; + private static readonly Dictionary httpApiProxyClassTable = []; + private const string HttpApiProxyClassTypeName = "WebApiClientCore.HttpApiProxyClass"; + + /// + /// 查找指定接口类型的代理类类型 + /// + /// 接口类型 + /// + public static Type? Find(Type httpApiType) + { + lock (syncRoot) + { + if (assemblies.Add(httpApiType.Assembly)) + { + AnalyzeAssembly(httpApiType.Assembly); + } + } + + if (httpApiProxyClassTable.TryGetValue(httpApiType, out var proxyClassType)) + { + return proxyClassType; + } + return null; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "类型已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075", Justification = "类型已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] + private static void AnalyzeAssembly(Assembly assembly) + { + var httpApiProxyClass = assembly.GetType(HttpApiProxyClassTypeName); + if (httpApiProxyClass != null) + { + foreach (var classType in httpApiProxyClass.GetNestedTypes(BindingFlags.NonPublic)) + { + var proxyClassAttr = classType.GetCustomAttribute(); + if (proxyClassAttr != null && proxyClassAttr.HttpApiType.IsAssignableFrom(classType)) + { + httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); + } + } + } + } + } +} diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs deleted file mode 100644 index 4fd029e9..00000000 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; - -namespace WebApiClientCore.Implementations -{ - /// - /// 提供获取SourceGenerator生成的代理类型 - /// - static class SourceGeneratorProxyClassType - { - private static readonly object syncRoot = new(); - private static readonly HashSet assemblies = []; - private static readonly ConcurrentDictionary httpApiProxyClassTable = []; - - /// - /// 查找指定接口类型的代理类类型 - /// - /// 接口类型 - /// - public static Type? Find(Type httpApiType) - { - AnalyzeAssembly(httpApiType.Assembly); - - if (httpApiProxyClassTable.TryGetValue(httpApiType, out var proxyClassType)) - { - return proxyClassType; - } - return null; - } - - - private static void AnalyzeAssembly(Assembly assembly) - { - if (AddAssembly(assembly)) - { - foreach (var classType in assembly.GetTypes()) - { - if (classType.IsClass) - { - var proxyClassAttr = classType.GetCustomAttribute(); - if (proxyClassAttr != null) - { - httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); - } - } - } - } - } - - private static bool AddAssembly(Assembly assembly) - { - lock (syncRoot) - { - return assemblies.Add(assembly); - } - } - } -} diff --git a/WebApiClientCore/Implementations/Tasks/AcitonRetryTask.cs b/WebApiClientCore/Implementations/Tasks/ActionRetryTask.cs similarity index 93% rename from WebApiClientCore/Implementations/Tasks/AcitonRetryTask.cs rename to WebApiClientCore/Implementations/Tasks/ActionRetryTask.cs index d33f44e2..bdc23d1c 100644 --- a/WebApiClientCore/Implementations/Tasks/AcitonRetryTask.cs +++ b/WebApiClientCore/Implementations/Tasks/ActionRetryTask.cs @@ -8,7 +8,7 @@ namespace WebApiClientCore.Implementations.Tasks /// 表示支持重试的Api请求任务 /// /// 结果类型 - sealed class AcitonRetryTask : TaskBase, IRetryTask + sealed class ActionRetryTask : TaskBase, IRetryTask { /// /// 请求任务创建的委托 @@ -32,7 +32,7 @@ sealed class AcitonRetryTask : TaskBase, IRetryTask /// 最大尝试次数 /// 各次重试的延时时间 /// - public AcitonRetryTask(Func> invoker, int maxRetryCount, Func? retryDelay) + public ActionRetryTask(Func> invoker, int maxRetryCount, Func? retryDelay) { if (maxRetryCount < 1) { @@ -54,7 +54,7 @@ protected override async Task InvokeAsync() { try { - await this.DelayBeforRetry(i).ConfigureAwait(false); + await this.DelayBeforeRetry(i).ConfigureAwait(false); return await this.invoker.Invoke().ConfigureAwait(false); } catch (RetryMarkException ex) @@ -71,7 +71,7 @@ protected override async Task InvokeAsync() /// /// /// - private async Task DelayBeforRetry(int index) + private async Task DelayBeforeRetry(int index) { if (index == 0 || this.retryDelay == null) { @@ -116,7 +116,7 @@ public IRetryTask WhenCatch(Action handler) whe /// 当捕获到异常时进行Retry /// /// 异常类型 - /// 返回true才Retry + /// 返回 true 才Retry /// public IRetryTask WhenCatch(Func predicate) where TException : Exception { @@ -149,7 +149,7 @@ public IRetryTask WhenCatchAsync(Func han /// 当捕获到异常时进行Retry /// /// 异常类型 - /// 返回true才Retry + /// 返回 true 才Retry /// public IRetryTask WhenCatchAsync(Func> predicate) where TException : Exception { @@ -168,7 +168,7 @@ async Task newInvoker() throw; } } - return new AcitonRetryTask(newInvoker, this.maxRetryCount, this.retryDelay); + return new ActionRetryTask(newInvoker, this.maxRetryCount, this.retryDelay); } /// @@ -213,7 +213,7 @@ async Task newInvoker() return result; } - return new AcitonRetryTask(newInvoker, this.maxRetryCount, this.retryDelay); + return new ActionRetryTask(newInvoker, this.maxRetryCount, this.retryDelay); } /// diff --git a/WebApiClientCore/Internals/HttpUtil.cs b/WebApiClientCore/Internals/HttpUtil.cs index 40d7d2e3..3e05411d 100644 --- a/WebApiClientCore/Internals/HttpUtil.cs +++ b/WebApiClientCore/Internals/HttpUtil.cs @@ -17,7 +17,7 @@ public static class HttpUtil /// 字符串 /// 编码 /// - [return: NotNullIfNotNull("str")] + [return: NotNullIfNotNull(nameof(str))] public static string? UrlEncode(string? str, Encoding encoding) { if (string.IsNullOrEmpty(str)) @@ -29,13 +29,13 @@ public static class HttpUtil var source = str.Length > 1024 ? new byte[byteCount] : stackalloc byte[byteCount]; encoding.GetBytes(str, source); - var destLength = 0; - if (UrlEncodeTest(source, ref destLength) == false) + var destinationLength = 0; + if (UrlEncodeTest(source, ref destinationLength) == false) { return str; } - var destination = destLength > 1024 ? new byte[destLength] : stackalloc byte[destLength]; + var destination = destinationLength > 1024 ? new byte[destinationLength] : stackalloc byte[destinationLength]; UrlEncodeCore(source, destination); return Encoding.ASCII.GetString(destination); } @@ -56,16 +56,16 @@ public static void UrlEncode(ReadOnlySpan chars, IBufferWriter buffe var source = chars.Length > 1024 ? new byte[byteCount] : stackalloc byte[byteCount]; Encoding.UTF8.GetBytes(chars, source); - var destLength = 0; - if (UrlEncodeTest(source, ref destLength) == false) + var destinationLength = 0; + if (UrlEncodeTest(source, ref destinationLength) == false) { bufferWriter.Write(source); } else { - var destination = bufferWriter.GetSpan(destLength); + var destination = bufferWriter.GetSpan(destinationLength); UrlEncodeCore(source, destination); - bufferWriter.Advance(destLength); + bufferWriter.Advance(destinationLength); } } @@ -74,23 +74,23 @@ public static void UrlEncode(ReadOnlySpan chars, IBufferWriter buffe /// 测试是否需要进行编码 /// /// 源 - /// 编码后的长度 - private static bool UrlEncodeTest(ReadOnlySpan source, ref int destLength) + /// 编码后的长度 + private static bool UrlEncodeTest(ReadOnlySpan source, ref int destinationLength) { - destLength = 0; + destinationLength = 0; if (source.IsEmpty == true) { return false; } var cUnsafe = 0; - var hasSapce = false; + var hasSpace = false; for (var i = 0; i < source.Length; i++) { var ch = (char)source[i]; if (ch == ' ') { - hasSapce = true; + hasSpace = true; } else if (!IsUrlSafeChar(ch)) { @@ -98,18 +98,18 @@ private static bool UrlEncodeTest(ReadOnlySpan source, ref int destLength) } } - destLength = source.Length + cUnsafe * 2; - return !(hasSapce == false && cUnsafe == 0); + destinationLength = source.Length + cUnsafe * 2; + return !(hasSpace == false && cUnsafe == 0); } /// - /// 将source编码到destination + /// 将 source 编码到 destination /// /// 源 /// 目标 private static void UrlEncodeCore(ReadOnlySpan source, Span destination) - { + { var index = 0; for (var i = 0; i < source.Length; i++) { @@ -134,7 +134,7 @@ private static void UrlEncodeCore(ReadOnlySpan source, Span destinat } /// - /// 是否为uri安全字符 + /// 是否为Uri安全字符 /// /// /// @@ -171,11 +171,7 @@ private static bool IsUrlSafeChar(char ch) private static char ToCharLower(int n) { n &= 0xF; - if (n > 9) - { - return (char)(n - 10 + 97); - } - return (char)(n + 48); + return n > 9 ? (char)(n - 10 + 97) : (char)(n + 48); } } } diff --git a/WebApiClientCore/Internals/LambdaUtil.cs b/WebApiClientCore/Internals/LambdaUtil.cs index 65647349..002144b6 100644 --- a/WebApiClientCore/Internals/LambdaUtil.cs +++ b/WebApiClientCore/Internals/LambdaUtil.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -57,7 +58,7 @@ public static Action CreateSetAction /// 属性 /// - /// + /// public static Func CreateGetFunc(PropertyInfo property) { if (property == null) @@ -65,38 +66,13 @@ public static Func CreateGetFunc(P throw new ArgumentNullException(nameof(property)); } - if (property.DeclaringType == null) - { - throw new ArgumentNullException(nameof(property)); - } - - return CreateGetFunc(property.DeclaringType, property.Name, property.PropertyType); - } - - /// - /// 创建属性的获取委托 - /// - /// - /// - /// 实例的类型 - /// 属性的名称 - /// 属性的类型 - /// - /// - public static Func CreateGetFunc(Type declaringType, string propertyName, Type? propertyType = null) - { + var declaringType = property.DeclaringType; if (declaringType == null) { - throw new ArgumentNullException(nameof(declaringType)); - } - - if (string.IsNullOrEmpty(propertyName) == true) - { - throw new ArgumentNullException(nameof(propertyName)); + throw new ArgumentException("DeclaringType can not be null", nameof(property)); } // (TDeclaring instance) => (propertyType)((declaringType)instance).propertyName - var paramInstance = Expression.Parameter(typeof(TDeclaring)); var bodyInstance = (Expression)paramInstance; @@ -105,8 +81,8 @@ public static Func CreateGetFunc(T bodyInstance = Expression.Convert(bodyInstance, declaringType); } - var bodyProperty = (Expression)Expression.Property(bodyInstance, propertyName); - if (typeof(TProperty) != propertyType) + var bodyProperty = (Expression)Expression.Property(bodyInstance, property); + if (typeof(TProperty) != property.PropertyType) { bodyProperty = Expression.Convert(bodyProperty, typeof(TProperty)); } @@ -114,7 +90,6 @@ public static Func CreateGetFunc(T return Expression.Lambda>(bodyProperty, paramInstance).Compile(); } - /// /// 创建字段的获取委托 /// @@ -156,7 +131,8 @@ public static Func CreateGetFunc(FieldIn /// /// 类型 /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { return CreateCtorFunc>(type, Array.Empty()); } @@ -170,7 +146,8 @@ public static Func CreateCtorFunc(Type type) /// /// /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { var args = new Type[] { typeof(TArg1) }; return CreateCtorFunc>(type, args); @@ -186,7 +163,8 @@ public static Func CreateCtorFunc(Type type) /// /// /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { var args = new Type[] { typeof(TArg1), typeof(TArg2) }; return CreateCtorFunc>(type, args); @@ -201,7 +179,7 @@ public static Func CreateCtorFunc(Type /// /// /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { var args = new Type[] { typeof(TArg1), typeof(TArg2), typeof(TArg3) }; return CreateCtorFunc>(type, args); @@ -216,7 +194,9 @@ public static Func CreateCtorFunc /// /// - private static TFunc CreateCtorFunc(Type type, Type[] args) + private static TFunc CreateCtorFunc( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, + Type[] args) { if (type == null) { diff --git a/WebApiClientCore/Internals/MediaTypeUtil.cs b/WebApiClientCore/Internals/MediaTypeUtil.cs index 2ee6e522..22015453 100644 --- a/WebApiClientCore/Internals/MediaTypeUtil.cs +++ b/WebApiClientCore/Internals/MediaTypeUtil.cs @@ -35,40 +35,9 @@ public static bool IsMatch(ReadOnlySpan source, ReadOnlySpan target) /// private static bool MediaTypeMatch(ReadOnlySpan source, ReadOnlySpan target) { - if (source.Length == 1 && source[0] == '*') - { - return true; - } - - if (target.Length == 1 && target[0] == '*') - { - return true; - } - - if (source.Length != target.Length) - { - return false; - } - - for (var i = 0; i < source.Length; i++) - { - var s = source[i]; - var t = target[i]; - if (char.IsUpper(s)) - { - s = char.ToLowerInvariant(s); - } - if (char.IsUpper(t)) - { - t = char.ToLowerInvariant(t); - } - - if (s.Equals(t) == false) - { - return false; - } - } - return true; + return source.Length == 1 && source[0] == '*' || + target.Length == 1 && target[0] == '*' || + source.Equals(target, StringComparison.OrdinalIgnoreCase); } } } diff --git a/WebApiClientCore/Internals/UriValue.cs b/WebApiClientCore/Internals/UriValue.cs index ad691224..59497e81 100644 --- a/WebApiClientCore/Internals/UriValue.cs +++ b/WebApiClientCore/Internals/UriValue.cs @@ -57,7 +57,7 @@ public UriValue Replace(string name, string? value, out bool replaced) value = Uri.EscapeDataString(value); } - var newUri = uriString.RepaceIgnoreCase($"{{{name}}}", value, out replaced); + var newUri = uriString.ReplaceIgnoreCase($"{{{name}}}", value, out replaced); return replaced ? new UriValue(newUri) : this; } @@ -71,7 +71,7 @@ public UriValue AddQuery(string name, string? value) { var uriSpan = this.uriString.AsSpan(); var fragmentSpan = GetFragment(uriSpan); - var baseSpan = uriSpan.Slice(0, uriSpan.Length - fragmentSpan.Length).TrimEnd('?').TrimEnd('&'); + var baseSpan = uriSpan[..^fragmentSpan.Length].TrimEnd('?').TrimEnd('&'); var concat = baseSpan.LastIndexOf('?') < 0 ? '?' : '&'; var nameSpan = Uri.EscapeDataString(name); var valueSpan = string.IsNullOrEmpty(value) @@ -105,18 +105,14 @@ private static ReadOnlySpan GetFragment(ReadOnlySpan uriSpan) } /// - /// 返回uriString是否不带path + /// 返回 uriString是否不带 path /// - /// + /// /// - private static bool IsEmptyPath(ReadOnlySpan uriSpan) + private static bool IsEmptyPath(ReadOnlySpan uriString) { - var index = uriSpan.IndexOf("://"); - if (index < 0) - { - return false; - } - return uriSpan[(index + 3)..].IndexOf('/') < 0; + var index = uriString.IndexOf("://"); + return index >= 0 && uriString[(index + 3)..].IndexOf('/') < 0; } /// diff --git a/WebApiClientCore/Internals/ValueStringBuilder.cs b/WebApiClientCore/Internals/ValueStringBuilder.cs index ff6ac301..db4eeed7 100644 --- a/WebApiClientCore/Internals/ValueStringBuilder.cs +++ b/WebApiClientCore/Internals/ValueStringBuilder.cs @@ -24,11 +24,10 @@ public ValueStringBuilder(Span buffer) } /// - /// 添加char + /// 添加 char /// - /// - /// - public ValueStringBuilder Append(char value) + /// + public void Append(char value) { var newSize = this.index + 1; if (newSize > this.chars.Length) @@ -38,19 +37,18 @@ public ValueStringBuilder Append(char value) this.chars[this.index..][0] = value; this.index = newSize; - return this; } /// - /// 添加chars + /// 添加 chars /// /// /// - public ValueStringBuilder Append(ReadOnlySpan value) + public void Append(ReadOnlySpan value) { if (value.IsEmpty) { - return this; + return; } var newSize = this.index + value.Length; @@ -61,7 +59,17 @@ public ValueStringBuilder Append(ReadOnlySpan value) value.CopyTo(this.chars[this.index..]); this.index = newSize; - return this; + } + + /// + /// 添加 chars 并换行 + /// + /// + /// + public void AppendLine(ReadOnlySpan value) + { + this.Append(value); + this.Append(Environment.NewLine); } /// @@ -90,7 +98,7 @@ private void Grow(int newSize) /// public override readonly string ToString() { - return this.chars.Slice(0, this.index).ToString(); + return this.chars[..this.index].ToString(); } } } diff --git a/WebApiClientCore/JsonString.cs b/WebApiClientCore/JsonString.cs index 3279828b..2db37977 100644 --- a/WebApiClientCore/JsonString.cs +++ b/WebApiClientCore/JsonString.cs @@ -20,7 +20,7 @@ interface IJsonString /// /// 表示Json字符串 - /// 该字符串为Value对象的json文本 + /// 该字符串为Value对象的 json 文本 /// /// public sealed class JsonString : IJsonString diff --git a/WebApiClientCore/NugetPackage/tools/install.ps1 b/WebApiClientCore/NugetPackage/tools/install.ps1 deleted file mode 100644 index 8178834f..00000000 --- a/WebApiClientCore/NugetPackage/tools/install.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -param($installPath, $toolsPath, $package, $project) - -$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve - -foreach($analyzersPath in $analyzersPaths) -{ - # Install the language agnostic analyzers. - if (Test-Path $analyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) - { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) - } - } - } -} - -# $project.Type gives the language name like (C# or VB.NET) -$languageFolder = "" -if($project.Type -eq "C#") -{ - $languageFolder = "cs" -} -if($project.Type -eq "VB.NET") -{ - $languageFolder = "vb" -} -if($languageFolder -eq "") -{ - return -} - -foreach($analyzersPath in $analyzersPaths) -{ - # Install language specific analyzers. - $languageAnalyzersPath = join-path $analyzersPath $languageFolder - if (Test-Path $languageAnalyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) - { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) - } - } - } -} \ No newline at end of file diff --git a/WebApiClientCore/NugetPackage/tools/uninstall.ps1 b/WebApiClientCore/NugetPackage/tools/uninstall.ps1 deleted file mode 100644 index 9130bcb5..00000000 --- a/WebApiClientCore/NugetPackage/tools/uninstall.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -param($installPath, $toolsPath, $package, $project) - -$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve - -foreach($analyzersPath in $analyzersPaths) -{ - # Uninstall the language agnostic analyzers. - if (Test-Path $analyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) - { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) - } - } - } -} - -# $project.Type gives the language name like (C# or VB.NET) -$languageFolder = "" -if($project.Type -eq "C#") -{ - $languageFolder = "cs" -} -if($project.Type -eq "VB.NET") -{ - $languageFolder = "vb" -} -if($languageFolder -eq "") -{ - return -} - -foreach($analyzersPath in $analyzersPaths) -{ - # Uninstall language specific analyzers. - $languageAnalyzersPath = join-path $analyzersPath $languageFolder - if (Test-Path $languageAnalyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) - { - if($project.Object.AnalyzerReferences) - { - try - { - $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) - } - catch - { - - } - } - } - } -} \ No newline at end of file diff --git a/WebApiClientCore/Parameters/FormDataFile.cs b/WebApiClientCore/Parameters/FormDataFile.cs index 659d0d9f..33624bf5 100644 --- a/WebApiClientCore/Parameters/FormDataFile.cs +++ b/WebApiClientCore/Parameters/FormDataFile.cs @@ -1,17 +1,14 @@ using System; -using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; using WebApiClientCore.HttpContents; -using WebApiClientCore.Internals; namespace WebApiClientCore.Parameters { /// - /// 表示form-data的一个文件项 + /// 表示 multipart/form-data 的一个文件项 /// [DebuggerDisplay("FileName = {FileName}")] public class FormDataFile : IApiParameter @@ -22,7 +19,7 @@ public class FormDataFile : IApiParameter private readonly Func streamFactory; /// - /// 获取文件好友名称 + /// 获取文件名称 /// public string? FileName { get; } @@ -30,16 +27,9 @@ public class FormDataFile : IApiParameter /// 获取或设置文件的Mime /// public string? ContentType { get; set; } = FormDataFileContent.OctetStream; - - /// - /// 获取编码后的文件好友名称 - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public virtual string? EncodedFileName => HttpUtil.UrlEncode(this.FileName, Encoding.UTF8); - + /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 文件路径 public FormDataFile(string filePath) @@ -48,7 +38,7 @@ public FormDataFile(string filePath) } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 文件信息 public FormDataFile(FileInfo fileInfo) @@ -57,7 +47,7 @@ public FormDataFile(FileInfo fileInfo) } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 数据 /// 文件友好名称 @@ -68,7 +58,7 @@ public FormDataFile(byte[] buffer, string? fileName) : } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// 不支持多线程并发请求 /// 如果多次请求则要求数据流必须支持倒带读取 /// @@ -81,7 +71,7 @@ public FormDataFile(Stream seekableStream, string? fileName) } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 数据流的创建委托 /// 文件友好名称 @@ -219,7 +209,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio /// /// 不释放资源 - /// 而是尝试倒带内置的stream以支持重新读取 + /// 而是尝试倒带内置的Stream以支持重新读取 /// /// protected override void Dispose(bool disposing) diff --git a/WebApiClientCore/Parameters/JsonPatchDocument.cs b/WebApiClientCore/Parameters/JsonPatchDocument.cs index 87e0461e..4c95e927 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Threading.Tasks; using WebApiClientCore.Exceptions; -using WebApiClientCore.HttpContents; namespace WebApiClientCore.Parameters { @@ -12,12 +14,17 @@ namespace WebApiClientCore.Parameters /// 表示将自身作为JsonPatch请求内容 /// [DebuggerTypeProxy(typeof(DebugView))] - public class JsonPatchDocument : IApiParameter + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext.")] + public class JsonPatchDocument : IApiParameter, IChunkedable { + private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new("application/json-patch+json"); + private readonly List operations = []; + /// - /// 操作列表 + /// 获取或设置是否允许 chunked 传输 + /// 默认为 true /// - private readonly List oprations = new(); + public bool AllowChunked { get; set; } = true; /// /// Add操作 @@ -31,7 +38,7 @@ public void Add(string path, object? value) { throw new ArgumentNullException(nameof(path)); } - this.oprations.Add(new { op = "add", path, value }); + this.operations.Add(new { op = "add", path, value }); } /// @@ -45,7 +52,7 @@ public void Remove(string path) { throw new ArgumentNullException(nameof(path)); } - this.oprations.Add(new { op = "remove", path }); + this.operations.Add(new { op = "remove", path }); } /// @@ -60,7 +67,7 @@ public void Replace(string path, object? value) { throw new ArgumentNullException(nameof(path)); } - this.oprations.Add(new { op = "replace", path, value }); + this.operations.Add(new { op = "replace", path, value }); } /// @@ -68,6 +75,8 @@ public void Replace(string path, object? value) /// /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("Trimming", "IL3050:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public Task OnRequestAsync(ApiParameterContext context) { if (context.HttpContext.RequestMessage.Method != HttpMethod.Patch) @@ -76,8 +85,14 @@ public Task OnRequestAsync(ApiParameterContext context) } var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = new JsonPatchContent(this.oprations, options); - + if (this.AllowChunked) + { + context.HttpContext.RequestMessage.Content = JsonContent.Create(this.operations, this.operations.GetType(), mediaTypeHeaderValue, options); + } + else + { + context.HttpContext.RequestMessage.Content = new HttpContents.JsonPatchContent(this.operations, options); + } return Task.CompletedTask; } @@ -95,7 +110,7 @@ private class DebugView /// 查看的内容 /// [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public List Oprations => this.target.oprations; + public List Operations => this.target.operations; /// /// 调试视图 diff --git a/WebApiClientCore/Serialization/JsonBufferSerializer.cs b/WebApiClientCore/Serialization/JsonBufferSerializer.cs index 44771922..9ceb707a 100644 --- a/WebApiClientCore/Serialization/JsonBufferSerializer.cs +++ b/WebApiClientCore/Serialization/JsonBufferSerializer.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace WebApiClientCore.Serialization @@ -14,11 +15,13 @@ public static class JsonBufferSerializer private static readonly JsonSerializerOptions defaultOptions = new(); /// - /// 将对象序列化为utf8编码的Json到指定的bufferWriter + /// 将对象序列化为Utf8编码的Json到指定的BufferWriter /// /// buffer写入器 /// 对象 /// 选项 + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static void Serialize(IBufferWriter bufferWriter, object? obj, JsonSerializerOptions? options) { if (obj == null) @@ -26,16 +29,9 @@ public static void Serialize(IBufferWriter bufferWriter, object? obj, Json return; } - var jsonOptions = options ?? defaultOptions; - var writerOptions = new JsonWriterOptions - { - SkipValidation = true, - Encoder = jsonOptions.Encoder, - Indented = jsonOptions.WriteIndented - }; - - using var utf8JsonWriter = new Utf8JsonWriter(bufferWriter, writerOptions); - JsonSerializer.Serialize(utf8JsonWriter, obj, obj.GetType(), jsonOptions); + options ??= defaultOptions; + var utf8JsonWriter = Utf8JsonWriterCache.Get(bufferWriter, options); + JsonSerializer.Serialize(utf8JsonWriter, obj, obj.GetType(), options); } } } \ No newline at end of file diff --git a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs index 94bbdea6..1c51d0f0 100644 --- a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs +++ b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -30,10 +31,9 @@ public override bool CanConvert(Type typeToConvert) /// /// /// - /// -#if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors, typeof(Converter<>))] -#endif + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(Converter<>))] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { return typeof(Converter<>).MakeGenericType(typeToConvert).CreateInstance(); @@ -46,13 +46,16 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer private class Converter : JsonConverter where TJsonString : IJsonString { /// - /// 将json文本反序列化JsonString的Value的类型 + /// 将Json文本反序列化JsonString的Value的类型 /// 并构建JsonString类型并返回 /// /// /// /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "")] public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var json = reader.GetString(); @@ -62,11 +65,13 @@ public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, } /// - /// 将JsonString的value序列化文本,并作为json的某字段值 + /// 将JsonString的Value序列化文本,并作为Json的某字段值 /// /// /// /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public override void Write(Utf8JsonWriter writer, TJsonString value, JsonSerializerOptions options) { if (value == null || value.Value == null) diff --git a/WebApiClientCore/Serialization/KeyValueSerializer.cs b/WebApiClientCore/Serialization/KeyValueSerializer.cs index d26da116..d159e048 100644 --- a/WebApiClientCore/Serialization/KeyValueSerializer.cs +++ b/WebApiClientCore/Serialization/KeyValueSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Text.Json; @@ -25,6 +26,8 @@ public static class KeyValueSerializer /// 对象实例 /// 选项 /// + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static IList Serialize(string key, object? obj, KeyValueSerializerOptions? options) { var kvOptions = options ?? defaultOptions; @@ -54,7 +57,7 @@ public static IList Serialize(string key, object? obj, KeyValueSeriali if (obj is IEnumerable> keyValues) { - // 排除字典类型,字典类型要经过json序列化 + // 排除字典类型,字典类型要经过Json序列化 if (objType.IsInheritFrom() == false) { // key的值不经过PropertyNamingPolicy转换,保持原始值 @@ -73,18 +76,14 @@ public static IList Serialize(string key, object? obj, KeyValueSeriali /// 对象类型 /// 选项 /// - private static IList GetKeyValueList(string key, object obj, Type objType, KeyValueSerializerOptions options) + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + private static List GetKeyValueList(string key, object obj, Type objType, KeyValueSerializerOptions options) { - var jsonOptions = options.GetJsonSerializerOptions(); using var bufferWriter = new RecyclableBufferWriter(); - using var utf8JsonWriter = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions - { - Indented = false, - SkipValidation = true, - Encoder = jsonOptions.Encoder - }); - - System.Text.Json.JsonSerializer.Serialize(utf8JsonWriter, obj, objType, jsonOptions); + var jsonOptions = options.GetJsonSerializerOptions(); + var utf8JsonWriter = Utf8JsonWriterCache.Get(bufferWriter, jsonOptions); + JsonSerializer.Serialize(utf8JsonWriter, obj, objType, jsonOptions); var utf8JsonReader = new Utf8JsonReader(bufferWriter.WrittenSpan, new JsonReaderOptions { MaxDepth = jsonOptions.MaxDepth, @@ -98,12 +97,12 @@ private static IList GetKeyValueList(string key, object obj, Type objT } /// - /// 获取shortName键值对 + /// 获取ShortName键值对 /// /// /// /// - private static IList GetShortNameKeyValueList(string key, ref Utf8JsonReader reader) + private static List GetShortNameKeyValueList(string key, ref Utf8JsonReader reader) { var list = new List(); while (reader.Read()) @@ -145,13 +144,13 @@ private static IList GetShortNameKeyValueList(string key, ref Utf8Json } /// - /// 获取fullName键值对 + /// 获取FullName键值对 /// /// /// /// /// - private static IList GetFullNameKeyValueList(string key, ref Utf8JsonReader reader, KeyNamingOptions options) + private static List GetFullNameKeyValueList(string key, ref Utf8JsonReader reader, KeyNamingOptions options) { using var doc = JsonDocument.ParseValue(ref reader); var root = doc.RootElement; diff --git a/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs b/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs new file mode 100644 index 00000000..2ec8063a --- /dev/null +++ b/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs @@ -0,0 +1,58 @@ +using System; +using System.Buffers; +using System.Text.Json; + +namespace WebApiClientCore.Serialization +{ + /// + /// Utf8JsonWriter缓存 + /// + static class Utf8JsonWriterCache + { + [ThreadStatic] + private static Utf8JsonWriter? threadUtf8JsonWriter; + + /// + /// 获取与 bufferWriter 关联的 Utf8JsonWriter + /// + /// + /// + /// + public static Utf8JsonWriter Get(IBufferWriter bufferWriter, JsonSerializerOptions options) + { + var utf8JsonWriter = threadUtf8JsonWriter; + if (utf8JsonWriter == null) + { + utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); + threadUtf8JsonWriter = utf8JsonWriter; + } + else if (OptionsEquals(utf8JsonWriter.Options, options)) + { + utf8JsonWriter.Reset(bufferWriter); + } + else + { + utf8JsonWriter.Dispose(); + utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); + threadUtf8JsonWriter = utf8JsonWriter; + } + return utf8JsonWriter; + } + + + private static bool OptionsEquals(JsonWriterOptions options1, JsonSerializerOptions options2) + { + return options1.Encoder == options2.Encoder && options1.Indented == options2.WriteIndented; + } + + private static JsonWriterOptions GetJsonWriterOptions(JsonSerializerOptions options) + { + return new JsonWriterOptions + { + Encoder = options.Encoder, + Indented = options.WriteIndented, + SkipValidation = true, + }; + } + } +} diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index ee3c6e59..f4d68b9d 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -1,7 +1,12 @@ using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Xml; +using WebApiClientCore.Internals; namespace WebApiClientCore.Serialization { @@ -14,11 +19,12 @@ public static class XmlSerializer private static readonly XmlWriterSettings writerSettings = new(); /// - /// 将对象序列化为xml文本 + /// 将对象序列化为Xml文本 /// /// 对象 /// 配置选项 /// + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] public static string? Serialize(object? obj, XmlWriterSettings? options) { if (obj == null) @@ -26,21 +32,40 @@ public static class XmlSerializer return null; } + using var bufferWriter = new RecyclableBufferWriter(); + Serialize(obj, options, bufferWriter); + return bufferWriter.WrittenSpan.ToString(); + } + + /// + /// 将对象序列化为Xml文本 + /// + /// buffer写入器 + /// 对象 + /// 配置选项 + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] + public static void Serialize(object? obj, XmlWriterSettings? options, IBufferWriter bufferWriter) + { + if (obj == null) + { + return; + } + var settings = options ?? writerSettings; - var writer = new EncodingWriter(settings.Encoding); + using var writer = new XmlBufferWriter(bufferWriter, settings.Encoding); using var xmlWriter = XmlWriter.Create(writer, settings); var xmlSerializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); xmlSerializer.Serialize(xmlWriter, obj); - return writer.ToString(); } /// - /// 将xml文本反序列化对象 + /// 将Xml文本反序列化对象 /// /// xml文本内容 /// 对象类型 /// 配置选项 /// + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] public static object? Deserialize(string? xml, Type objType, XmlReaderSettings? options) { if (objType == null) @@ -60,28 +85,101 @@ public static class XmlSerializer return xmlSerializer.Deserialize(xmlReader); } - /// - /// 表示可指定编码文本写入器 - /// - private class EncodingWriter : StringWriter + + private class XmlBufferWriter : TextWriter { - /// - /// 编码 - /// - private readonly Encoding encoding; - - /// - /// 获取编码 - /// - public override Encoding Encoding => this.encoding; - - /// - /// 可指定编码文本写入器 - /// - /// 编码 - public EncodingWriter(Encoding encoding) - { - this.encoding = encoding; + private readonly IBufferWriter bufferWriter; + + public override Encoding Encoding { get; } + + public XmlBufferWriter(IBufferWriter bufferWriter, Encoding encoding) + { + this.bufferWriter = bufferWriter; + this.Encoding = encoding; + } + + public override Task FlushAsync() + { + return Task.CompletedTask; + } + + public override void Write(ReadOnlySpan buffer) + { + this.bufferWriter.Write(buffer); + } + + public override void Write(char value) + { + Span buffer = [value]; + this.Write(buffer); + } + + public override void Write(char[] buffer, int index, int count) + { + this.Write(buffer.AsSpan(index, count)); + } + public override void Write(string? value) + { + this.Write(value.AsSpan()); + } + + public override Task WriteAsync(string? value) + { + this.Write(value.AsSpan()); + return Task.CompletedTask; + } + + public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + this.Write(buffer.Span); + return Task.CompletedTask; + } + + public override Task WriteAsync(char value) + { + Span buffer = [value]; + this.Write(buffer); + return Task.CompletedTask; + } + + public override Task WriteAsync(char[] buffer, int index, int count) + { + this.Write(buffer.AsSpan(index, count)); + return Task.CompletedTask; + } + + public override void WriteLine(ReadOnlySpan buffer) + { + this.Write(buffer); + WriteLine(); + } + + public override Task WriteLineAsync(string? value) + { + this.Write(value.AsSpan()); + WriteLine(); + return Task.CompletedTask; + } + public override Task WriteLineAsync(char value) + { + Span buffer = [value]; + this.Write(buffer); + WriteLine(); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(char[] buffer, int index, int count) + { + this.Write(buffer.AsSpan(0, count)); + WriteLine(); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + this.Write(buffer.Span); + WriteLine(); + return Task.CompletedTask; } } } diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index d0ecbd5d..b9a19da6 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,6 +1,10 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; +using WebApiClientCore; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -11,23 +15,41 @@ namespace System.Net.Http /// public static class HttpContentExtensions { + private const string IsBufferedPropertyName = "IsBuffered"; + private const string IsBufferedGetMethodName = "get_IsBuffered"; + /// /// IsBuffered字段 /// - private static readonly Func? isBuffered; + private static readonly Func? isBufferedFunc; /// /// 静态构造器 - /// + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, typeof(HttpContent))] static HttpContentExtensions() { - var property = typeof(HttpContent).GetProperty("IsBuffered", BindingFlags.Instance | BindingFlags.NonPublic); + var property = typeof(HttpContent).GetProperty(IsBufferedPropertyName, BindingFlags.Instance | BindingFlags.NonPublic); if (property != null) { - isBuffered = LambdaUtil.CreateGetFunc(property); +#if NET8_0_OR_GREATER + if (property.GetGetMethod(nonPublic: true)?.Name == IsBufferedGetMethodName) + { + isBufferedFunc = GetIsBuffered; + } +#endif + if (isBufferedFunc == null) + { + isBufferedFunc = LambdaUtil.CreateGetFunc(property); + } } } +#if NET8_0_OR_GREATER + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = IsBufferedGetMethodName)] + private static extern bool GetIsBuffered(HttpContent httpContent); +#endif + /// /// 获取是否已缓存数据 /// @@ -35,11 +57,11 @@ static HttpContentExtensions() /// public static bool? IsBuffered(this HttpContent httpContent) { - return isBuffered?.Invoke(httpContent); + return isBufferedFunc == null ? null : isBufferedFunc(httpContent); } /// - /// 确保httpContent的内容未被缓存 + /// 确保HttpContent的内容未被缓存 /// 已被缓存则抛出HttpContentBufferedException /// /// @@ -53,14 +75,14 @@ public static void EnsureNotBuffered(this HttpContent httpContent) } /// - /// 读取为二进制数组并转换为utf8编码 + /// 读取为二进制数组并转换为 utf8 编码 /// /// /// /// public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent) { - return httpContent.ReadAsByteArrayAsync(Encoding.UTF8); + return httpContent.ReadAsByteArrayAsync(Encoding.UTF8, default); } /// @@ -70,14 +92,24 @@ public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent /// 目标编码 /// /// - public static async Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding) + public static Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding) { - var encoding = httpContent.GetEncoding(); - var byteArray = await httpContent.ReadAsByteArrayAsync().ConfigureAwait(false); + return httpContent.ReadAsByteArrayAsync(dstEncoding, default); + } - return encoding.Equals(dstEncoding) - ? byteArray - : Encoding.Convert(encoding, dstEncoding, byteArray); + /// + /// 读取为二进制数组并转换为指定的编码 + /// + /// + /// 目标编码 + /// + /// + /// + public static async Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding, CancellationToken cancellationToken) + { + var encoding = httpContent.GetEncoding(); + var byteArray = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + return encoding.Equals(dstEncoding) ? byteArray : Encoding.Convert(encoding, dstEncoding, byteArray); } /// @@ -87,19 +119,23 @@ public static async Task ReadAsByteArrayAsync(this HttpContent httpConte /// public static Encoding GetEncoding(this HttpContent httpContent) { - var charSet = httpContent.Headers.ContentType?.CharSet; - if (string.IsNullOrEmpty(charSet) == true) + var contentType = httpContent.Headers.ContentType; + if (contentType == null) { return Encoding.UTF8; } - var span = charSet.AsSpan().TrimStart('"').TrimEnd('"'); - if (span.Equals(Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase)) + var charSet = contentType.CharSet.AsSpan(); + if (charSet.IsEmpty) { return Encoding.UTF8; } - return Encoding.GetEncoding(span.ToString()); + var encoding = charSet.Trim('"'); + return encoding.Equals(Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) + ? Encoding.UTF8 + : Encoding.GetEncoding(encoding.ToString()); } + } } diff --git a/WebApiClientCore/System.Net.Http/HttpProgress.cs b/WebApiClientCore/System.Net.Http/HttpProgress.cs index fb7878ac..6527d633 100644 --- a/WebApiClientCore/System.Net.Http/HttpProgress.cs +++ b/WebApiClientCore/System.Net.Http/HttpProgress.cs @@ -1,7 +1,7 @@ namespace System.Net.Http { /// - /// 表示http进度 + /// 表示 http 进度 /// public class HttpProgress { diff --git a/WebApiClientCore/System.Net.Http/HttpRequestMessageExtensions.cs b/WebApiClientCore/System.Net.Http/HttpRequestMessageExtensions.cs index a480bffc..6923664d 100644 --- a/WebApiClientCore/System.Net.Http/HttpRequestMessageExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpRequestMessageExtensions.cs @@ -1,4 +1,4 @@ -using System.Text; +using WebApiClientCore.Internals; namespace System.Net.Http { @@ -15,7 +15,8 @@ public static class HttpRequestMessageExtensions public static string GetHeadersString(this HttpRequestMessage request) { var uri = request.RequestUri; - var builder = new StringBuilder(); + Span buffer = stackalloc char[4 * 1024]; + var builder = new ValueStringBuilder(buffer); if (uri != null && uri.IsAbsoluteUri) { diff --git a/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs b/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs index 4217e7cf..f37423ec 100644 --- a/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs @@ -1,7 +1,8 @@ using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; +using WebApiClientCore; +using WebApiClientCore.Internals; namespace System.Net.Http { @@ -17,9 +18,11 @@ public static class HttpResponseMessageExtensions /// public static string GetHeadersString(this HttpResponseMessage response) { - var builder = new StringBuilder() - .AppendLine($"HTTP/{response.Version} {(int)response.StatusCode} {response.ReasonPhrase}") - .Append(response.Headers.ToString()); + Span buffer = stackalloc char[4 * 1024]; + var builder = new ValueStringBuilder(buffer); + + builder.AppendLine($"HTTP/{response.Version} {(int)response.StatusCode} {response.ReasonPhrase}"); + builder.Append(response.Headers.ToString()); if (response.Content != null) { @@ -71,11 +74,7 @@ public static async Task SaveAsAsync(this HttpResponseMessage httpResponse, Stre throw new ArgumentNullException(nameof(destination)); } -#if NET5_0_OR_GREATER var source = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); -#else - var source = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); -#endif await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false); } @@ -102,18 +101,11 @@ public static async Task SaveAsAsync(this HttpResponseMessage httpResponse, Stre var fileSize = httpResponse.Content.Headers.ContentLength; var buffer = new byte[8 * 1024]; -#if NET5_0_OR_GREATER + var source = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); -#else - var source = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); -#endif while (isCompleted == false && cancellationToken.IsCancellationRequested == false) { -#if NET5_0_OR_GREATER - var length = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); -#else var length = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); -#endif if (length == 0) { fileSize ??= recvSize; @@ -122,11 +114,7 @@ public static async Task SaveAsAsync(this HttpResponseMessage httpResponse, Stre else { recvSize += length; -#if NET5_0_OR_GREATER - await destination.WriteAsync(buffer.AsMemory(0, length), cancellationToken).ConfigureAwait(false); -#else await destination.WriteAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); -#endif await destination.FlushAsync(cancellationToken).ConfigureAwait(false); } diff --git a/WebApiClientCore/TaskExtenstions.cs b/WebApiClientCore/TaskExtenstions.cs index a9217300..92c1506e 100644 --- a/WebApiClientCore/TaskExtenstions.cs +++ b/WebApiClientCore/TaskExtenstions.cs @@ -58,7 +58,7 @@ public static IRetryTask Retry(this ITask task, int m { throw new ArgumentOutOfRangeException(nameof(maxCount)); } - return new AcitonRetryTask(async () => await task, maxCount, delay); + return new ActionRetryTask(async () => await task, maxCount, delay); } /// @@ -84,11 +84,9 @@ public static ITask HandleAsDefaultWhenException(this ITask public static IHandleTask Handle(this ITask task) { - if (task == null) - { - throw new ArgumentNullException(nameof(task)); - } - return new ActionHandleTask(async () => await task); + return task == null + ? throw new ArgumentNullException(nameof(task)) + : (IHandleTask)new ActionHandleTask(async () => await task); } } } diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 61f2116b..8e6d8bd3 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -2,31 +2,39 @@ enable - netstandard2.1;net5.0 - $(TargetPath)\$(AssemblyName).xml + True + netstandard2.1;net5.0;net8.0 + true .NetCore声明式的Http客户端库 一款基于HttpClient封装,只需要定义c#接口并修饰相关特性,即可异步调用远程http接口的客户端库 true Sign.snk - + - - + + - - - - + + + + + + + + + + + @@ -43,12 +51,8 @@ + - + - - - - - diff --git a/docs/Readme.md b/docs/Readme.md deleted file mode 100644 index 9ab7868d..00000000 --- a/docs/Readme.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -home: true -heroImage: /logo.png -heroText: WebApiClient -tagline: 使用C#接口描述你的http接口 -actions: - - text: 快速开始 💡 - link: /guide/ - type: primary - - text: 安装 - link: /reference/nuget - type: default - - text: 旧版文档 - link: /old/ - type: default -features: - - title: AOT/JIT - details: ⛳ 支持编译时,运行时生成代理类,提高运行时性能和兼容性 - - title: 多样序列化 - details: 🛠 默认System.Text.Json,Newtonsoft.Json,同样也支持XML处理 - - title: 语法分析 - details: 💡提供接口声明的语法分析与提示,帮助开发者声明接口时避免使用不当的语法。 - - title: 功能完备 - details: 🌳支持多种拦截器和过滤器、日志、重试、缓存、异常处理功能 - - title: 快速接入 - details: ✒ 支持OAuth2与token管理扩展包,方便实现身份认证和授权 - - title: 自动生成 - details: 💻 支持将本地或远程OpenApi文档解析生成WebApiClientCore接口代码的dotnet tool,简化接口声明的工作量 -footer: MIT Licensed | Copyright © WebApiClient ---- \ No newline at end of file diff --git a/docs/guide/1_getting-started.md b/docs/guide/1_getting-started.md new file mode 100644 index 00000000..f3a27e11 --- /dev/null +++ b/docs/guide/1_getting-started.md @@ -0,0 +1,135 @@ +# 快速上手 + +## 依赖环境 + +`WebApiclientCore`要求项目的`.NET`版本支持`.NET Standard2.1`,并且具备依赖注入的环境。 + +## 从 Nuget 安装 + +| 包名 | Nuget | 描述 | +| ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [WebApiClientCore](https://www.nuget.org/packages/WebApiClientCore) | ![NuGet logo](https://buildstats.info/nuget/WebApiClientCore) | 基础包 | +| [WebApiClientCore.Extensions.OAuths](https://www.nuget.org/packages/WebApiClientCore.Extensions.OAuths) | ![NuGet logo](https://buildstats.info/nuget/WebApiClientCore.Extensions.OAuths) | OAuth2 与 token 管理扩展包 | +| [WebApiClientCore.Extensions.NewtonsoftJson](https://www.nuget.org/packages/WebApiClientCore.Extensions.NewtonsoftJson) | ![NuGet logo](https://buildstats.info/nuget/WebApiClientCore.Extensions.NewtonsoftJson) | Newtonsoft 的 Json.NET 扩展包 | +| [WebApiClientCore.Extensions.JsonRpc](https://www.nuget.org/packages/WebApiClientCore.Extensions.JsonRpc) | ![NuGet logo](https://buildstats.info/nuget/WebApiClientCore.Extensions.JsonRpc) | JsonRpc 调用扩展包 | +| [WebApiClientCore.OpenApi.SourceGenerator](https://www.nuget.org/packages/WebApiClientCore.OpenApi.SourceGenerator) | ![NuGet logo](https://buildstats.info/nuget/WebApiClientCore.OpenApi.SourceGenerator) | 将本地或远程 OpenApi 文档解析生成 WebApiClientCore 接口代码的 dotnet tool | + +## 声明接口 + +```csharp +[LoggingFilter] +[HttpHost("http://localhost:5000/")] +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id); + + [HttpPost("api/users")] + Task PostAsync([JsonContent] User user); +} + +public class User +{ + [JsonPropertyName("account")] + public string Account { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; +} +``` + +## 注册和配置接口 + +AspNetCore Startup + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddHttpApi().ConfigureHttpApi(o => + { + o.UseLogging = Environment.IsDevelopment(); + o.HttpHost = new Uri("http://localhost:5000/"); + + // o.JsonSerializeOptions -> json 序列化选项 + // o.JsonDeserializeOptions -> json 反序列化选项 + // o.KeyValueSerializeOptions -> 键值对序列化选项 + // o.XmlSerializeOptions -> xml 序列化选项 + // o.XmlDeserializeOptions -> xml 反序列化选项 + // o.GlobalFilters -> 全局过滤器集合 + }); +} +``` + +Console + +```csharp +public static void Main(string[] args) +{ + // 无依赖注入的环境需要自行创建 + var services = new ServiceCollection(); + services.AddHttpApi().ConfigureHttpApi(o => + { + o.UseLogging = Environment.IsDevelopment(); + o.HttpHost = new Uri("http://localhost:5000/"); + }); +} +``` + +## 全局配置接口 + +全局配置可以做为所有接口的默认初始配置,当项目中有很多接口时就很有用。 + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddWebApiClient().ConfigureHttpApi(o => + { + o.JsonSerializeOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + o.JsonDeserializeOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + o.KeyValueSerializeOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + }); +} +``` + +## 注入和调用接口 + +在Scoped或Transient服务中注入 + +```csharp +public class YourService +{ + private readonly IUserApi userApi; + public YourService(IUserApi userApi) + { + this.userApi = userApi; + } + + public async Task GetAsync() + { + // 调用接口 + var user = await this.userApi.GetAsync(id:"id001"); + ... + } +} +``` + +在Singleton服务中注入 + +```csharp +public class YourService +{ + private readonly IServiceScopeFactory serviceScopeFactory; + public YourService(IServiceScopeFactory serviceScopeFactory) + { + this.serviceScopeFactory = serviceScopeFactory; + } + + public async Task GetAsync() + { + // 从创建的scope中获取接口实例 + using var scope = this.serviceScopeFactory.CreateScope(); + var userApi = scope.ServiceProvider.GetRequiredService(); + var user = await userApi.GetAsync(id:"id001"); + ... + } +} +``` diff --git a/docs/guide/2_attributes.md b/docs/guide/2_attributes.md new file mode 100644 index 00000000..4d8cded3 --- /dev/null +++ b/docs/guide/2_attributes.md @@ -0,0 +1,451 @@ +# 内置特性 + +内置特性指框架内提供的一些特性,拿来即用就能满足一般情况下的各种应用。当然,开发者也可以在实际应用中,编写满足特定场景需求的特性,然后将自定义特性修饰到接口、方法或参数即可。 + +> 执行前顺序 + +参数值验证 -> IApiActionAttribute -> IApiParameterAttribute -> IApiReturnAttribute -> IApiFilterAttribute + +> 执行后顺序 + +IApiReturnAttribute -> 返回值验证 -> IApiFilterAttribute + +## 各特性的位置 + +```csharp +[IApiFilterAttribute]/*作用于接口内所有方法的Filter*/ +[IApiReturnAttribute]/*作用于接口内所有方法的ReturnAttribute*/ +public interface DemoApiInterface +{ + [IApiActionAttribute] + [IApiFilterAttribute]/*作用于本方法的Filter*/ + [IApiReturnAttribute]/*作用于本方法的ReturnAttribute*/ + Task DemoApiMethod([IApiParameterAttribute] ParameterClass parameterClass); +} +``` + +## Return 特性 + +Return特性用于处理响应内容为对应的.NET数据模型,其存在以下规则: + +1. 当其EnsureMatchAcceptContentType属性为true(默认值)时,其AcceptContentType属性值与响应的Content-Type值匹配时才生效。 +2. 当所有Return特性的AcceptContentType属性值都不匹配响应的Content-Type值时,引发`ApiReturnNotSupportedException` +3. 当其EnsureSuccessStatusCode属性为true(默认值)时,且响应的状态码不在200到299之间时,引发`ApiResponseStatusException`。 +4. 同一种AcceptContentType属性值的多个Return特性,只有AcceptQuality属性值最大的特性生效。 + +### 缺省的Return特性 + +在缺省情况下,每个接口的都已经隐性存在了多个AcceptQuality为0.1的Return特性,当你想修改某种Return特性的其它属性时,你只需要声明一个AcceptQuality值更大的同类型Return特性即可。 + +```csharp +[Json] // .AcceptQuality = 1.0, .EnsureSuccessStatusCode = true, .EnsureMatchAcceptContentType = false +/* 以下特性是隐性存在的 +[RawReturn(0.1, EnsureSuccessStatusCode = true, EnsureMatchAcceptContentType = true)] +[NoneReturn(0.1, EnsureSuccessStatusCode = true, EnsureMatchAcceptContentType = true)] +[JsonReturn(0.1, EnsureSuccessStatusCode = true, EnsureMatchAcceptContentType = true)] +[XmlReturn(0.1, EnsureSuccessStatusCode = true, EnsureMatchAcceptContentType = true)] +*/ +Task DemoApiMethod(); +``` + +### RawReturnAttribute + +表示原始类型的结果特性,支持结果类型为`string`、`byte[]`、`Stream`和`HttpResponseMessage` + +```csharp +[RawReturnAttribute] +Task DemoApiMethod(); +``` + +### JsonReturnAttribute + +表示json内容的结果特性,使用`System.Text.Json`进行序列化和反序列化 + +```csharp +[JsonReturnAttribute] +Task DemoApiMethod(); +``` + +### XmlReturnAttribute + +表示xml内容的结果特性,使用`System.Xml.Serialization`进行序列化和反序列化 + +```csharp +[XmlReturnAttribute] +Task DemoApiMethod(); +``` + +### NoneReturnAttribute + +表示响应状态为204时将结果设置为返回类型的默认值特性 + +```csharp +// if response status code is 204, return default value of return type +[NoneReturnAttribute] +Task DemoApiMethod(); +``` + +## Action 特性 + +### HttpHostAttribute + +当请求域名是已知的常量时,才能使用 HttpHost 特性。 + +```csharp +[HttpHost("http://localhost:5000/")] // 对接口下所有方法适用 +public interface IUserApi +{ + Task GetAsync(string id); + + [HttpHost("http://localhost:8000/")] // 会覆盖接口声明的HttpHost + Task PostAsync(User user); +} +``` + +### HttpGetAttribute + +GET请求 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] // 支持 null、绝对或相对路径 + Task GetAsync(string id); +} +``` + +### HttpPostAttribute + +POST请求 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] // 支持 null、绝对或相对路径 + Task PostAsync([JsonContent] User user); +} +``` + +### HttpPutAttribute + +PUT请求 + +```csharp +public interface IUserApi +{ + [HttpPut("api/users")] // 支持 null、绝对或相对路径 + Task PutAsync([JsonContent] User user); +} +``` + +### HttpDeleteAttribute + +DELETE请求 + +```csharp +public interface IUserApi +{ + [HttpDelete("api/users")] // 支持 null、绝对或相对路径 + Task DeleteAsync([JsonContent] User user); +} +``` + +### HttpPatchAttribute + +PATCH请求 + +```csharp +public interface IUserApi +{ + [HttpPatch("api/users/{id}")] + Task PatchAsync(string id, JsonPatchDocument doc); +} + +var doc = new JsonPatchDocument(); +doc.Replace(item => item.Account, "laojiu"); +doc.Replace(item => item.Email, "laojiu@qq.com"); +``` + +### HeaderAttribute + +常量值请求头。 + +```csharp +public interface IUserApi +{ + [Header("headerName1", "headerValue1")] + [Header("headerName2", "headerValue2")] + [HttpGet("api/users/{id}")] + Task GetAsync(string id); +} +``` + +### TimeoutAttribute + +常量值请求超时时长。 + +```csharp +public interface IUserApi +{ + [Timeout(10 * 1000)] // 超时时长为10秒 + [HttpGet("api/users/{id}")] + Task GetAsync(string id); +} +``` + +### FormFieldAttribute + +常量值 x-www-form-urlencoded 表单字段。 + +```csharp +public interface IUserApi +{ + [FormField("fieldName1", "fieldValue1")] + [FormField("fieldName2", "fieldValue2")] + [HttpPost("api/users")] + Task PostAsync([FormContent] User user); +} +``` + +### FormDataTextAttribute + +常量值 multipart/form-data 表单字段。 + +```csharp +public interface IUserApi +{ + [FormDataText("fieldName1", "fieldValue1")] + [FormDataText("fieldName2", "fieldValue2")] + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user); +} +``` + +## Parameter 特性 + +### PathQueryAttribute + +参数值的键值对作为请示 url 路径参数或 query 参数的特性,一般类型的参数,缺省特性时 PathQueryAttribute 会隐性生效。 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync([PathQuery] string id); +} +``` + +### FormContentAttribute + +参数值的键值对作为 x-www-form-urlencoded 表单。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user); +} +``` + +### FormFieldAttribute + +参数值作为 x-www-form-urlencoded 表单字段与值。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user, [FormField] string field1); +} +``` + +### FormDataContentAttribute + +参数值的键值对作为 multipart/form-data 表单。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user, /*表单文件*/ FormDataFile headImage); +} +``` + +### FormDataTextAttribute + +参数值作为 multipart/form-data 表单字段与值。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user, /*表单文件*/ FormDataFile headImage, [FormDataText] string field1); +} +``` + +### JsonContentAttribute + +参数值序列化为请求的 json 内容。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([JsonContent] User user); +} +``` + +### XmlContentAttribute + +参数值序列化为请求的 xml 内容。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([XmlContent] User user); +} +``` + +### UriAttribute + +参数值作为请求Uri,只能修饰第一个参数,可以是相对 Uri 或绝对 Uri。 + +```csharp +public interface IUserApi +{ + [HttpGet] + Task GetAsync([Uri] Uri uri); +} +``` + +### TimeoutAttribute + +参数值作为超时时间的毫秒数,值不能大于 HttpClient 的 Timeout 属性。 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id, [Timeout] int timeout); +} +``` + +### HeaderAttribute + +参数值作为请求头。 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id, [Header] string headerName1); +} +``` + +### HeadersAttribute + +参数值的键值对作为请求头。 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id, [Headers] CustomHeaders headers); + + [HttpGet("api/users/{id}")] + Task Get2Async(string id, [Headers] Dictionary headers); +} + +public class CustomHeaders +{ + public string HeaderName1 { get; set; } + public string HeaderName1 { get; set; } +} +``` + +### RawStringContentAttribute + +原始文本内容。 + +```csharp +public interface IUserApi +{ + [HttpPost] + Task PostAsync([RawStringContent("text/plain")] string text); +} +``` + +### RawJsonContentAttribute + +原始 json 内容。 + +```csharp +public interface IUserApi +{ + [HttpPost] + Task PostAsync([RawJsonContent] string json); +} +``` + +### RawXmlContentAttribute + +原始 xml 内容。 + +```csharp +public interface IUserApi +{ + [HttpPost] + Task PostAsync([RawXmlContent] string xml); +} +``` + +### RawFormContentAttribute + +原始 x-www-form-urlencoded 表单内容,这些内容要求是表单编码过的。 + +```csharp +public interface IUserApi +{ + [HttpPost] + Task PostAsync([RawFormContent] string form); +} +``` + +## Filter 特性 + +Filter特性可用于发送前最后一步的内容修改,或者查看响应数据内容。 + +### LoggingFilterAttribute + +请求和响应内容的输出为日志到 LoggingFactory。 + +```csharp +[LoggingFilter] // 所有方法都记录请求日志 +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id); + + [LoggingFilter(Enable = false)] // 本方法禁用日志 + [HttpPost("api/users")] + Task PostAsync([JsonContent] User user); +} +``` + +## Cache 特性 + +把本次的响应内容缓存起来,下一次如果符合预期条件的话,就不会再请求到远程服务器,而是从 IResponseCacheProvider 获取缓存内容,开发者可以自己实现 ResponseCacheProvider。 + +### CacheAttribute + +使用请求的完整 Uri 做为缓存的 Key 应用缓存。 + +```csharp +public interface IUserApi +{ + [Cache(60 * 1000)] // 缓存一分钟 + [HttpGet("api/users/{id}")] + Task GetAsync(string id); +} +``` diff --git a/docs/guide/3_special-type.md b/docs/guide/3_special-type.md new file mode 100644 index 00000000..eba68d60 --- /dev/null +++ b/docs/guide/3_special-type.md @@ -0,0 +1,75 @@ +# 特殊参数 + +特殊参数是指不需要任何特性来修饰就能工作的一些参数类型。 + +## CancellationToken 类型 + +每个接口都支持声明一个或多个 CancellationToken 类型的参数,用于取消请求操作。 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id, CancellationToken token = default); +} +``` + +## FileInfo 类型 + +做为 multipart/form-data 表单的一个文件项,实现文件上传功能。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user, FileInfo headImage); +} +``` + +## HttpContent 的子类型 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users/{id}")] + Task PostAsync(StringContent text); + + [HttpPost("api/users/{id}")] + Task PostAsync(StreamContent stream); + + [HttpPost("api/users/{id}")] + Task PostAsync(ByteArrayContent bytes); +} +``` + +## IApiParameter 的子类型 + +实现 IApiParameter 的类型,称为自解释参数类型,它可以弥补特性(Attribute)不能解决的一些复杂参数。 + +### FormDataFile 类型 + +做为 multipart/form-data 表单的一个文件项,实现文件上传功能,等效于 FileInfo 类型。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([FormDataContent] User user, FormDataFile headImage); +} +``` + +### JsonPatchDocument 类型 + +表示 JsonPatch 请求文档。 + +```csharp +public interface IUserApi +{ + [HttpPatch("api/users/{id}")] + Task PatchAsync(string id, JsonPatchDocument doc); +} + +var doc = new JsonPatchDocument(); +doc.Replace(item => item.Account, "laojiu"); +doc.Replace(item => item.Email, "laojiu@qq.com"); +``` diff --git a/docs/guide/4_data-validation.md b/docs/guide/4_data-validation.md new file mode 100644 index 00000000..720f5879 --- /dev/null +++ b/docs/guide/4_data-validation.md @@ -0,0 +1,54 @@ +# 数据验证 + +使用 ValidationAttribute 的子类特性来验证请求参数值和响应结果。 + +## 参数值验证 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{email}")] + Task GetAsync( + [EmailAddress, Required] // 这些验证特性用于请求前验证此参数 + string email); +} +``` + +## 请求或响应模型验证 + +请求和相应用到的 User 的两个属性值都得到验证。 + +```csharp +public interface IUserApi +{ + [HttpPost("api/users")] + Task PostAsync([Required][JsonContent] 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; } +} +``` + +## 关闭数据验证功能 + +数据验证功能默认是开启的,可以在接口的 HttpApiOptions 配置关闭数据验证功能。 + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddHttpApi().ConfigureHttpApi(o => + { + // 关闭数据验证功能,即使打了验证特性也不验证。 + o.UseParameterPropertyValidate = false; + o.UseReturnValuePropertyValidate = false; + }); +} +``` diff --git a/docs/guide/5_advanced.md b/docs/guide/5_advanced.md new file mode 100644 index 00000000..19aeebcb --- /dev/null +++ b/docs/guide/5_advanced.md @@ -0,0 +1,626 @@ +# 进阶功能 + +## Uri 拼接规则 + +所有的 Uri 拼接都是通过 new 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 的结尾的第一种方式。 + +## 请求异常处理 + +请求一个接口,不管出现何种异常,最终都抛出 HttpRequestException,HttpRequestException 的内部异常为实际具体异常,之所以设计为内部异常,是为了完好的保存内部异常的堆栈信息。 + +WebApiClientCore 内部的很多异常都基于 ApiException 这个异常抽象类,也就是很多情况下抛出的异常都是内部异常为某个 ApiException 的 HttpRequestException。 + +```csharp +try +{ + var datas = 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) +{ + // 异常 +} +``` + +## 请求条件性重试 + +使用`ITask<>`异步声明,就有 Retry 的扩展,Retry 的条件可以为捕获到某种 Exception 或响应模型符合某种条件。 + +```csharp +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + ITask GetAsync(string id); +} + +var result = await userApi.GetAsync(id: "id001") + .Retry(maxCount: 3) + .WhenCatch() + .WhenResult(r => r.Age <= 0); +``` + +`ITask<>`可以精确控制某个方法的重试逻辑,如果想全局性实现重试,请结合使用 [Polly](https://learn.microsoft.com/zh-cn/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly) 来实现。 + +## 表单集合处理 + +按照 OpenApi,一个集合在 Uri 的 Query 或表单中支持 5 种表述方式,分别是: + +- Csv // 逗号分隔 +- Ssv // 空格分隔 +- Tsv // 反斜杠分隔 +- Pipes // 竖线分隔 +- Multi // 多个同名键的键值对 + +对于 `id = ["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` | + +## 适配畸形接口 + +### 不友好的参数名别名 + +例如服务器要求一个 Query 参数的名字为`field-Name`,这个是`C#`关键字或变量命名不允许的,我们可以使用`[AliasAsAttribute]`来达到这个要求: + +```csharp +public interface IUserApi +{ + [HttpGet("api/users")] + ITask GetAsync([AliasAs("field-Name")] string fieldName); +} +``` + +然后最终请求 uri 变为 api/users/?field-name=`fileNameValue` + +### Form 的某个字段为 json 文本 + +| 字段 | 值 | +| ------ | ------------------------ | +| field1 | someValue | +| field2 | `{"name":"sb","age":18}` | + +field2 对应的 .NET 模型为 + +```csharp +public class Field2 +{ + public string Name {get; set;} + + public int Age {get; set;} +} +``` + +常规下我们得把 field2 的实例 json 序列化得到 json 文本,然后赋值给 field2 这个 string 属性,使用[JsonFormField]特性可以轻松帮我们自动完成 Field2 类型的 json 序列化并将结果字符串作为表单的一个字段。 + +```csharp +public interface IUserApi +{ + Task PostAsync([FormField] string field1, [JsonFormField] Field2 field2) +} +``` + +### Form 的字段多层嵌套 + +| 字段 | 值 | +| ----------- | --------- | +| field1 | someValue | +| field2.name | sb | +| field2.age | 18 | + + +Form 对应的 .NET 模型为 +```csharp +public class FormModel +{ + public string Field1 {get; set;} + + public Field2 Field2 {get; set;} +} + +public class Field2 +{ + public string Name {get; set;} + + public int Age {get; set;} +} +``` + +合理情况下,对于复杂嵌套结构的数据模型,应当设计为使用 applicaiton/json 提交 FormModel,但服务提供方设计为使用 x-www-form-urlencoded 来提交 FormModel,我可以配置 KeyValueSerializeOptions 来达到这个格式要求: + +```csharp +services.AddHttpApi().ConfigureHttpApi(o => +{ + o.KeyValueSerializeOptions.KeyNamingStyle = KeyNamingStyle.FullName; +}); +``` + +### 响应的 Content-Type 不是预期值 + +响应的内容通过肉眼看上是 json 内容,但响应头里的 Content-Type 为非预期值 application/json或 application/xml,而是诸如 text/html 等。这好比客户端提交 json 内容时指示请求头的 Content-Type 值为 text/plain 一样,让服务端无法处理。 + +解决办法是在 Interface 或 Method 声明`[JsonReturn]`特性,并设置其 EnsureMatchAcceptContentType 属性为 false,表示 Content-Type 不是期望值匹配也要处理。 + +```csharp +[JsonReturn(EnsureMatchAcceptContentType = false)] +public interface IUserApi +{ +} +``` + +## 动态 HttpHost + +### 使用 UriAttribute 传绝对 Uri 参 + +```csharp +[LoggingFilter] +public interface IUserApi +{ + [HttpGet] + ITask GetAsync([Uri] string urlString, [PathQuery] string id); +} +``` + +### 自定义 HttpHostBaseAttribute 实现 + +```csharp +[ServiceNameHost("baidu")] // 使用自定义的ServiceNameHostAttribute +public interface IUserApi +{ + [HttpGet("api/users/{id}")] + Task GetAsync(string id); + + [HttpPost("api/users")] + Task PostAsync([JsonContent] User user); +} + +/// +/// 以服务名来确定主机的特性 +/// +public class ServiceNameHostAttribute : HttpHostBaseAttribute +{ + public string ServiceName { get; } + + public ServiceNameHostAttribute(string serviceName) + { + this.ServiceName = serviceName; + } + + public override Task OnRequestAsync(ApiRequestContext context) + { + // HostProvider是你自己的服务,数据来源可以是db或其它等等,要求此服务已经注入了DI + HostProvider hostProvider = context.HttpContext.ServiceProvider.GetRequiredService(); + string host = hostProvider.ResolveHost(this.ServiceName); + + // 最终目的是设置请求消息的RequestUri的属性 + context.HttpContext.RequestMessage.RequestUri = new Uri(host); + } +} +``` + +## 请求签名 + +### 动态追加请求签名 + +例如每个请求的 Uri 额外的动态添加一个叫 sign 的 query 参数,这个 sign 可能和请求参数值有关联,每次都需要计算。 +我们可以自定义 ApiFilterAttribute 的子来实现自己的 sign 功能,然后把自定义 Filter 声明到 Interface 或 Method 即可 + +```csharp +public class SignFilterAttribute : ApiFilterAttribute +{ + public override Task OnRequestAsync(ApiRequestContext context) + { + var signService = context.HttpContext.ServiceProvider.GetRequiredService(); + var sign = signService.SignValue(DateTime.Now); + context.HttpContext.RequestMessage.AddUrlQuery("sign", sign); + return Task.CompletedTask; + } +} + +[SignFilter] +public interface IUserApi +{ + ... +} +``` + +### 请求表单的字段排序 + +不知道是哪门公司起的所谓的“签名算法”,往往要表单的字段排序等。 + +```csharp +public interface IUserApi +{ + [HttpGet("/path")] + Task PostAsync([SortedFormContent] Model model); +} + +public class SortedFormContentAttribute : FormContentAttribute +{ + protected override IEnumerable SerializeToKeyValues(ApiParameterContext context) + { + 这里可以排序、加上其它衍生字段等 + return base.SerializeToKeyValues(context).OrderBy(item => item.Key); + } +} + +``` + +## .NET8 AOT 发布 + +System.Text.Json 中使用[源生成功能](https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0)之后,使项目AOT发布成为可能。 + +json 序列化源生成示例 + +```csharp +[JsonSerializable(typeof(User[]))] // 这里要挂上所有接口中使用到的 json 模型类型 +[JsonSerializable(typeof(YourModel[]))] +public partial class AppJsonSerializerContext : JsonSerializerContext +{ +} +``` + +在 WebApiClientCore 的全局配置中添加 json 源生成的上下文 + +```csharp +services + .AddWebApiClient() + .ConfigureHttpApi(options => // json SG生成器配置 + { + options.PrependJsonSerializerContext(AppJsonSerializerContext.Default); + }); +``` + +## HttpClient 的配置 + +这部分是 [Httpclient Factory](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/httpclient-factory) 的内容,这里不做过多介绍。 + +```csharp +services.AddHttpApi().ConfigureHttpClient(httpClient => +{ + httpClient.Timeout = TimeSpan.FromMinutes(1d); + httpClient.DefaultRequestVersion = HttpVersion.Version20; + httpClient.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; +}); +``` + +## 主 HttpMessageHandler 的配置 + +### Http 代理配置 + +```csharp +services.AddHttpApi().ConfigureHttpApi(o => +{ + o.HttpHost = new Uri("http://localhost:5000/"); +}) +.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler +{ + UseProxy = true, + Proxy = new WebProxy + { + Address = new Uri("http://proxy.com"), + Credentials = new NetworkCredential + { + UserName = "useranme", + Password = "pasword" + } + } +}); +``` + +### 客户端证书配置 + +有些服务器为了限制客户端的连接,开启了 https 双向验证,只允许它执有它颁发的证书的客户端进行连接 + +```csharp +services.AddHttpApi().ConfigureHttpApi(o => +{ + o.HttpHost = new Uri("http://localhost:5000/"); +}) +.ConfigurePrimaryHttpMessageHandler(() => +{ + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(yourCert); + return handler; +}); +``` + +### 维持 CookieContainer 不变 + +如果请求的接口不幸使用了 Cookie 保存身份信息机制,那么就要考虑维持 CookieContainer 实例不要跟随 HttpMessageHandler 的生命周期,默认的 HttpMessageHandler 最短只有 2 分钟的生命周期。 + +```csharp +var cookieContainer = new CookieContainer(); +services.AddHttpApi().ConfigureHttpApi(o => +{ + o.HttpHost = new Uri("http://localhost:5000/"); +}) +.ConfigurePrimaryHttpMessageHandler(() => +{ + var handler = new HttpClientHandler(); + handler.CookieContainer = cookieContainer; + return handler; +}); +``` + +## 在接口配置中使用过滤器 +除了能在接口声明中使用 IApiFilterAttribute 子类的特性标注之外,还可以在接口注册时的配置添加 IApiFilter 类型的过滤器,这些过滤器将对整个接口生效,且优先于通过特性标注的 IApiFilterAttribute 类型执行。 +```csharp +services.AddHttpApi().ConfigureHttpApi(o => +{ + o.GlobalFilters.Add(new UserFiler()); +}); +``` + +```csharp +public class UserFiler : IApiFilter +{ + public Task OnRequestAsync(ApiRequestContext context) + { + throw new System.NotImplementedException(); + } + + public Task OnResponseAsync(ApiResponseContext context) + { + throw new System.NotImplementedException(); + } +} +``` + + +## 自定义请求内容与响应内容解析 + +除了常见的 xml 或 json 响应内容要反序列化为强类型结果模型,你可能会遇到其它的二进制协议响应内容,比如 google 的 ProtoBuf 二进制内容。 + +自定义请求内容处理特性 +```csharp +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; + } +} +``` + +自定义响应内容解析特性 +```csharp +public class ProtobufReturnAttribute : ApiReturnAttribute +{ + public ProtobufReturnAttribute(string acceptContentType = "application/x-protobuf") + : base(new MediaTypeWithQualityHeaderValue(acceptContentType)) + { + } + + public override async Task SetResultAsync(ApiResponseContext context) + { + var stream = await context.HttpContext.ResponseMessage.Content.ReadAsStreamAsync(); + context.Result = Serializer.NonGeneric.Deserialize(context.ApiAction.Return.DataType.Type, stream); + } +} +``` + +应用相关自定义特性 +```csharp +[ProtobufReturn] +public interface IProtobufApi +{ + [HttpPut("/users/{id}")] + Task UpdateAsync([Required, PathQuery] string id, [ProtobufContent] User user); +} +``` + +## 自定义 CookieAuthorizationHandler + +对于使用 Cookie 机制的接口,只有在接口请求之后,才知道 Cookie 是否已失效。通过自定义 CookieAuthorizationHandler,可以做在请求某个接口过程中,遇到 Cookie 失效时自动刷新 Cookie 再重试请求接口。 + +首先,我们需要把登录接口与某它业务接口拆分在不同的接口定义,例如 IUserApi 和 IUserLoginApi + +```csharp +[HttpHost("http://localhost:5000/")] +public interface IUserLoginApi +{ + [HttpPost("/users")] + Task LoginAsync([JsonContent] Account account); +} +``` + +然后实现自动登录的 CookieAuthorizationHandler + +```csharp +public class AutoRefreshCookieHandler : CookieAuthorizationHandler +{ + private readonly IUserLoginApi api; + + public AutoRefreshCookieHandler(IUserLoginApi api) + { + this.api = api; + } + + /// + /// 登录并刷新Cookie + /// + /// 返回登录响应消息 + protected override Task RefreshCookieAsync() + { + return this.api.LoginAsync(new Account + { + account = "admin", + password = "123456" + }); + } +} +``` + +最后,注册 IUserApi、IUserLoginApi,并为 IUserApi 配置 AutoRefreshCookieHandler + +```csharp +services + .AddHttpApi(); + +services + .AddHttpApi() + .AddHttpMessageHandler(s => new AutoRefreshCookieHandler(s.GetRequiredService())); +``` + +现在,调用 IUserApi 的任意接口,只要响应的状态码为 401,就触发 IUserLoginApi 登录,然后将登录得到的 cookie 来重试请求接口,最终响应为正确的结果。你也可以重写 CookieAuthorizationHandler 的 IsUnauthorizedAsync(HttpResponseMessage) 方法来指示响应是未授权状态。 + +## 自定义日志输出目标 + +```csharp +[CustomLogging] +public interface IUserApi +{ +} + + +public class CustomLoggingAttribute : LoggingFilterAttribute +{ + protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage) + { + // 这里把logMessage输出到你的目标 + return Task.CompletedTask; + } +} + +``` + +## 自定义缓存提供者 + +默认的缓存提供者为内存缓存,如果希望将缓存保存到其它存储位置,则需要自定义 缓存提者,并注册替换默认的缓存提供者。 + +```csharp +public static IWebApiClientBuilder UseRedisResponseCacheProvider(this IWebApiClientBuilder builder) +{ + builder.Services.AddSingleton(); + return builder; +} + +public class RedisResponseCacheProvider : IResponseCacheProvider +{ + public string Name => nameof(RedisResponseCacheProvider); + + public Task GetAsync(string key) + { + // 从redis获取缓存 + throw new NotImplementedException(); + } + + public Task SetAsync(string key, ResponseCacheEntry entry, TimeSpan expiration) + { + // 把缓存内容写入redis + throw new NotImplementedException(); + } +} +``` + +## 自定义自解释的参数类型 + +在某些极限情况下,比如人脸比对的接口,我们输入模型与传输模型未必是对等的,例如: + +服务端要求的 json 模型 + +```json +{ + "image1": "图片1的base64", + "image2": "图片2的base64" +} +``` + +客户端期望的业务模型 + +```csharp +public class FaceModel +{ + public Bitmap Image1 {get; set;} + public Bitmap Image2 {get; set;} +} +``` + +我们希望构造模型实例时传入 Bitmap 对象,但传输的时候变成 Bitmap 的 base64 值,所以我们要改造 FaceModel,让它实现 IApiParameter 接口: + +```csharp +public 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 options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; + context.HttpContext.RequestMessage.Content = new 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 来请求 + +```csharp +public interface IFaceApi +{ + [HttpPost("/somePath")] + Task PostAsync(FaceModel faces); +} +``` diff --git a/docs/guide/6_auth-token-extension.md b/docs/guide/6_auth-token-extension.md new file mode 100644 index 00000000..5628c1c0 --- /dev/null +++ b/docs/guide/6_auth-token-extension.md @@ -0,0 +1,205 @@ +# OAuths&Token 扩展 + +使用 WebApiClientCore.Extensions.OAuths 扩展,轻松支持 token 的获取、刷新与应用。 + +## 对象与概念 +### ITokenProviderFactory +ITokenProvider 的创建工厂,提供通过 HttpApi 接口类型获取或创建 ITokenProvider。 + +### ITokenProvider +token 提供者,用于获取 token,在 token 的过期后的头一次请求里触发重新请求或刷新 token。 + +### OAuthTokenAttribute +token 的应用特性,使用 ITokenProviderFactory 创建 ITokenProvider,然后使用 ITokenProvider 获取 token,最后将 token 应用到请求消息中。 + +### OAuthTokenHandler +属于 http 消息处理器,功能与 OAuthTokenAttribute 一样,除此之外,如果因为意外的原因导致服务器仍然返回未授权(401 状态码),其还会丢弃旧 token,申请新 token 来重试一次请求。 + + +## OAuth 的 Client 模式 + +### 为接口注册 TokenProvider + +```csharp +// 为接口注册与配置Client模式的tokenProvider +services.AddClientCredentialsTokenProvider(o => +{ + o.Endpoint = new Uri("http://localhost:6000/api/tokens"); + o.Credentials.Client_id = "clientId"; + o.Credentials.Client_secret = "xxyyzz"; +}); +``` + +### token 的应用 + +#### 使用 OAuthToken 特性 + +OAuthTokenAttribute 属于 WebApiClientCore 框架层,很容易操控请求内容和响应模型,比如将 token 作为表单字段添加到既有请求表单中,或者读取响应消息反序列化之后对应的业务模型都非常方便,但它不能在请求内部实现重试请求的效果。在服务器颁发 token 之后,如果服务器的 token 丢失了,使用 OAuthTokenAttribute 会得到一次失败的请求,本次失败的请求无法避免。 + +```csharp +/// +/// 用户操作接口 +/// +[OAuthToken] +public interface IUserApi +{ + ... +} +``` + +OAuthTokenAttribute 默认实现将 token 放到 Authorization 请求头,如果你的接口需要请 token 放到其它地方比如 Uri 的 Query,需要重写 OAuthTokenAttribute: + +```csharp +public class UriQueryTokenAttribute : OAuthTokenAttribute +{ + protected override void UseTokenResult(ApiRequestContext context, TokenResult tokenResult) + { + context.HttpContext.RequestMessage.AddUrlQuery("mytoken", tokenResult.Access_token); + } +} + +[UriQueryToken] +public interface IUserApi +{ + ... +} +``` + +#### 使用 OAuthTokenHandler + +OAuthTokenHandler 的强项是支持在一个请求内部里进行多次尝试,在服务器颁发 token 之后,如果服务器的 token 丢失了,OAuthTokenHandler 在收到 401 状态码之后,会在本请求内部丢弃和重新请求 token,并使用新 token 重试请求,从而表现为一次正常的请求。但 OAuthTokenHandler 不属于 WebApiClientCore 框架层的对象,在里面只能访问原始的 HttpRequestMessage 与 HttpResponseMessage,如果需要将 token 追加到 HttpRequestMessage 的 Content 里,这是非常困难的,同理,如果不是根据 http 状态码(401 等)作为 token 无效的依据,而是使用 HttpResponseMessage 的 Content 对应的业务模型的某个标记字段,也是非常棘手的活。 + +```csharp +// 注册接口时添加OAuthTokenHandler +services + .AddHttpApi() + .AddOAuthTokenHandler(); +``` + +OAuthTokenHandler 默认实现将 token 放到 Authorization 请求头,如果你的接口需要请 token 放到其它地方比如 uri 的 query,需要重写 OAuthTokenHandler: + +```csharp +public class UriQueryOAuthTokenHandler : OAuthTokenHandler +{ + /// + /// token应用的http消息处理程序 + /// + /// token提供者 + public UriQueryOAuthTokenHandler(ITokenProvider tokenProvider) + : base(tokenProvider) + { + } + + /// + /// 应用token + /// + /// + /// + 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; + + var uriValue = new UriValue(request.RequestUri); + uriValue = uriValue.AddQuery("myToken", tokenResult.Access_token); + request.RequestUri = uriValue.ToUri(); + } +} + + +// 注册接口时添加UriQueryOAuthTokenHandler +services + .AddHttpApi() + .AddOAuthTokenHandler((s, tp) => new UriQueryOAuthTokenHandler(tp)); +``` + +## 多接口共享的 TokenProvider + +可以给 http 接口设置基础接口,然后为基础接口配置 TokenProvider,例如下面的 xxx 和 yyy 接口,都属于 IBaidu,只需要给 IBaidu 配置 TokenProvider。 + +```csharp +[OAuthToken] +public interface IBaidu +{ +} + +public interface IBaidu_XXX_Api : IBaidu +{ + [HttpGet] + Task xxxAsync(); +} + +public interface IBaidu_YYY_Api : IBaidu +{ + [HttpGet] + Task yyyAsync(); +} +``` + +```csharp +// 注册与配置password模式的token提者选项 +services.AddPasswordCredentialsTokenProvider(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 的接口如下: + +```csharp +public interface ITokenApi +{ + [HttpPost("http://xxx.com/token")] + Task RequestTokenAsync([Parameter(Kind.Form)] string clientId, [Parameter(Kind.Form)] string clientSecret); +} +``` + +### 委托 TokenProvider + +委托 TokenProvider 是一种最简单的实现方式,它将请求 token 的委托作为自定义 TokenProvider 的实现逻辑: + +```csharp +// 为接口注册自定义tokenProvider +services.AddTokenProvider(s => +{ + return s.GetRequiredService().RequestTokenAsync("id", "secret"); +}); +``` + +### 完整实现的 TokenProvider + +```csharp +// 为接口注册CustomTokenProvider +services.AddTokenProvider(); +``` + +```csharp +public class CustomTokenProvider : TokenProvider +{ + public CustomTokenProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + protected override Task RequestTokenAsync(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService().RequestTokenAsync("id", "secret"); + } + + protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) + { + return this.RequestTokenAsync(serviceProvider); + } +} +``` + +### 自定义 TokenProvider 的选项 + +每个 TokenProvider 都有一个 Name 属性,与 service.AddTokenProvider()返回的 ITokenProviderBuilder 的 Name 是同一个值。读取 Options 值可以使用 TokenProvider 的 GetOptionsValue()方法,配置 Options 则通过 ITokenProviderBuilder 的 Name 来配置。 diff --git a/docs/guide/7_json-net-extension.md b/docs/guide/7_json-net-extension.md new file mode 100644 index 00000000..5e840f60 --- /dev/null +++ b/docs/guide/7_json-net-extension.md @@ -0,0 +1,29 @@ +# Json.NET 扩展 + +使用 WebApiClientCore.Extensions.NewtonsoftJson 扩展,轻松支持 Newtonsoft 的 `Json.NET` 来序列化和反序列化 json。 + +## 配置[可选] + +```csharp +// ConfigureNewtonsoftJson +services.AddHttpApi().ConfigureNewtonsoftJson(o => +{ + o.JsonSerializeOptions.NullValueHandling = NullValueHandling.Ignore; +}); +``` + +## 声明特性 + +使用[JsonNetReturn]替换内置的[JsonReturn],[JsonNetContent]替换内置[JsonContent] + +```csharp +/// +/// 用户操作接口 +/// +[JsonNetReturn] +public interface IUserApi +{ + [HttpPost("/users")] + Task PostAsync([JsonNetContent] User user); +} +``` diff --git a/docs/guide/json-rpc.md b/docs/guide/8_jsonrpc-extension.md similarity index 56% rename from docs/guide/json-rpc.md rename to docs/guide/8_jsonrpc-extension.md index e302b87f..54a26f56 100644 --- a/docs/guide/json-rpc.md +++ b/docs/guide/8_jsonrpc-extension.md @@ -1,21 +1,20 @@ - -# JsonRpc调用 +# JsonRpc 扩展 -在极少数场景中,开发者可能遇到JsonRpc调用的接口,由于该协议不是很流行,WebApiClientCore将该功能的支持作为WebApiClientCore.Extensions.JsonRpc扩展包提供。使用[JsonRpcMethod]修饰Rpc方法,使用[JsonRpcParam]修饰Rpc参数 +在极少数场景中,开发者可能遇到 JsonRpc 调用的接口,由于该协议不是很流行,WebApiClientCore 将该功能的支持作为 WebApiClientCore.Extensions.JsonRpc 扩展包提供。使用[JsonRpcMethod]修饰 Rpc 方法,使用[JsonRpcParam]修饰 Rpc 参数 即可。 -## JsonRpc声明 +## JsonRpc 声明 ```csharp [HttpHost("http://localhost:5000/jsonrpc")] -public interface IUserApi +public interface IUserApi { [JsonRpcMethod("add")] ITask> AddAsync([JsonRpcParam] string name, [JsonRpcParam] int age, CancellationToken token = default); } ``` -## JsonRpc数据包 +## JsonRpc 数据包 ```log diff --git a/docs/guide/9_openapi-to-code.md b/docs/guide/9_openapi-to-code.md new file mode 100644 index 00000000..fd292d91 --- /dev/null +++ b/docs/guide/9_openapi-to-code.md @@ -0,0 +1,32 @@ +# 将OpenApi(swagger)生成代码 + +使用这个工具可以将 OpenApi 的本地或远程文档解析生成 WebApiClientCore 的接口定义代码文件,`ASP.NET Core` 的 swagger json 文件也适用 + +## 安装工具 + +```shell +dotnet tool install WebApiClientCore.OpenApi.SourceGenerator -g +``` + +## 使用工具 + +运行以下命令,会将对应的 WebApiClientCore 的接口定义代码文件输出到当前目录的 output 文件夹下 + +```shell +#举例 +WebApiClientCore.OpenApi.SourceGenerator -o https://petstore.swagger.io/v2/swagger.json +``` + +### 命令介绍 + +```text + -o OpenApi, --openapi=OpenApi Required. openApi的json本地文件路径或远程Uri地址 + -n Namespace, --namespace=Namespace 代码的命名空间,如WebApiClientCore + --help Display this help screen. +``` + +### 工具原理 +1. 使用 NSwag 解析 OpenApi 的 json 得到 OpenApiDocument 对象 +2. 使用 RazorEngine 将 OpenApiDocument 传入 cshtml 模板编译得到 html +3. 使用 XDocument 将 html 的文本代码提取,得到 WebApiClientCore 的声明式代码 +4. 代码美化,输出到本地文件 diff --git a/docs/guide/attribute.md b/docs/guide/attribute.md deleted file mode 100644 index 60281c26..00000000 --- a/docs/guide/attribute.md +++ /dev/null @@ -1,65 +0,0 @@ - -# 常用内置特性 - -内置特性指框架内提供的一些特性,拿来即用就能满足一般情况下的各种应用。当然,开发者也可以在实际应用中,编写满足特定场景需求的特性,然后将自定义特性修饰到接口、方法或参数即可。 - -> 执行前顺序 - -参数值验证 -> IApiActionAttribute -> IApiParameterAttribute -> IApiReturnAttribute -> IApiFilterAttribute - -> 执行后顺序 - -IApiReturnAttribute -> 返回值验证 -> IApiFilterAttribute - -## Return特性 - -特性名称 | 功能描述 | 备注 ----|---|---| -RawReturnAttribute | 处理原始类型返回值 | 缺省也生效 -JsonReturnAttribute | 处理Json模型返回值 | 缺省也生效 -XmlReturnAttribute | 处理Xml模型返回值 | 缺省也生效 -NoneReturnAttribute | 处理空返回值 | 缺省也生效 - -## 常用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请求文档 | 无需特性修饰 diff --git a/docs/guide/config.md b/docs/guide/config.md deleted file mode 100644 index 0ea40fa6..00000000 --- a/docs/guide/config.md +++ /dev/null @@ -1,51 +0,0 @@ -# 配置 - -## 全局配置 - -2.0以后的版本,提供services.AddWebApiClient()的全局配置功能,支持提供自定义的IHttpApiActivator<>、IApiActionDescriptorProvider、IApiActionInvokerProvider和IResponseCacheProvider。 - -## 接口注册与选项 - -调用`services.AddHttpApi()`即可完成接口注册, -每个接口的选项对应为`HttpApiOptions`,选项名称通过HttpApi.GetName()方法获取得到。 - -## 在IHttpClientBuilder配置 - -```csharp -services - .AddHttpApi() - .ConfigureHttpApi(Configuration.GetSection(nameof(IUserApi))) - .ConfigureHttpApi(o => - { - // 符合国情的不标准时间格式,有些接口就是这么要求必须不标准 - o.JsonSerializeOptions.Converters.Add(new JsonDateTimeConverter("yyyy-MM-dd HH:mm:ss")); - }); -``` - -配置文件的json - -```json -{ - "IUserApi": { - "HttpHost": "http://www.webappiclient.com/", - "UseParameterPropertyValidate": false, - "UseReturnValuePropertyValidate": false, - "JsonSerializeOptions": { - "IgnoreNullValues": true, - "WriteIndented": false - } - } -} -``` - -## 在IServiceCollection配置 - -```csharp -services - .ConfigureHttpApi(Configuration.GetSection(nameof(IUserApi))) - .ConfigureHttpApi(o => - { - // 符合国情的不标准时间格式,有些接口就是这么要求必须不标准 - o.JsonSerializeOptions.Converters.Add(new JsonDateTimeConverter("yyyy-MM-dd HH:mm:ss")); - }); -``` diff --git a/docs/guide/core-analyzers.md b/docs/guide/core-analyzers.md deleted file mode 100644 index 2041ceb3..00000000 --- a/docs/guide/core-analyzers.md +++ /dev/null @@ -1,6 +0,0 @@ -# 编译时语法分析 - -WebApiClientCore.Analyzers提供接口声明的语法分析与提示,帮助开发者声明接口时避免使用不当的语法。 - -* 1.x版本,接口继承IHttpApi才获得语法分析提示 -* 2.0以后的版本,不继承IHttpApi也获得语法分析提示 diff --git a/docs/guide/data-validation.md b/docs/guide/data-validation.md deleted file mode 100644 index 679b1daf..00000000 --- a/docs/guide/data-validation.md +++ /dev/null @@ -1,34 +0,0 @@ -# 数据验证 - -## 参数值验证 - -对于参数值,支持ValidationAttribute特性修饰来验证值。 - -```csharp -public interface IUserApi -{ - [HttpGet("api/users/{email}")] - Task GetAsync([EmailAddress, Required] string email); -} -``` - -## 参数或返回模型属性验证 - -```csharp -public interface IUserApi -{ - [HttpPost("api/users")] - Task 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; } -} -``` diff --git a/docs/guide/deformed-interface.md b/docs/guide/deformed-interface.md deleted file mode 100644 index f8fc421f..00000000 --- a/docs/guide/deformed-interface.md +++ /dev/null @@ -1,132 +0,0 @@ -# 适配畸形接口 - -在实际应用场景中,常常会遇到一些设计不标准的畸形接口,主要是早期还没有restful概念时期的接口,我们要区分分析这些接口,包装为友好的客户端调用接口。 - -## 不友好的参数名别名 - -例如服务器要求一个Query参数的名字为`field-Name`,这个是c#关键字或变量命名不允许的,我们可以使用`[AliasAsAttribute]`来达到这个要求: - -```csharp -public interface IDeformedApi -{ - [HttpGet("api/users")] - ITask GetAsync([AliasAs("field-Name")] string fieldName); -} -``` - -然后最终请求uri变为api/users/?field-name=`fileNameValue` - -## Form的某个字段为json文本 - -字段 | 值 ----|--- -field1 | someValue -field2 | {"name":"sb","age":18} - -对应强类型模型是 - -```csharp -class Field2 -{ - public string Name {get; set;} - - public int Age {get; set;} -} -``` - -常规下我们得把field2的实例json序列化得到json文本,然后赋值给field2这个string属性,使用[JsonFormField]特性可以轻松帮我们自动完成Field2类型的json序列化并将结果字符串作为表单的一个字段。 - -```csharp -public interface IDeformedApi -{ - Task PostAsync([FormField] string field1, [JsonFormField] Field2 field2) -} -``` - -## Form提交嵌套的模型 - -字段 | 值 ----|---| -|filed1 |someValue| -|field2.name | sb| -|field2.age | 18| - -其对应的json格式为 - -```json -{ - "field1" : "someValue", - "filed2" : { - "name" : "sb", - "age" : 18 - } -} -``` - -合理情况下,对于复杂嵌套结构的数据模型,应当使用applicaiton/json,但接口要求必须使用Form提交,我可以配置KeyValueSerializeOptions来达到这个格式要求: - -```csharp -services.AddHttpApi(o => -{ - o.KeyValueSerializeOptions.KeyNamingStyle = KeyNamingStyle.FullName; -}); -``` - -## 响应未指明ContentType - -明明响应的内容肉眼看上是json内容,但服务响应头里没有ContentType告诉客户端这内容是json,这好比客户端使用Form或json提交时就不在请求头告诉服务器内容格式是什么,而是让服务器猜测一样的道理。 - -解决办法是在Interface或Method声明`[JsonReturn]`特性,并设置其EnsureMatchAcceptContentType属性为false,表示ContentType不是期望值匹配也要处理。 - -```csharp -[JsonReturn(EnsureMatchAcceptContentType = false)] -public interface IDeformedApi -{ -} -``` - -## 类签名参数或apikey参数 - -例如每个请求的url额外的动态添加一个叫sign的参数,这个sign可能和请求参数值有关联,每次都需要计算。 - -我们可以自定义ApiFilterAttribute来实现自己的sign功能,然后把自定义Filter声明到Interface或Method即可 - -```csharp -class SignFilterAttribute : ApiFilterAttribute -{ - public override Task OnRequestAsync(ApiRequestContext context) - { - var signService = context.HttpContext.ServiceProvider.GetService(); - var sign = signService.SignValue(DateTime.Now); - context.HttpContext.RequestMessage.AddUrlQuery("sign", sign); - return Task.CompletedTask; - } -} - -[SignFilter] -public interface IDeformedApi -{ - ... -} -``` - -## 表单字段排序 - -不知道是哪门公司起的所谓的“签名算法”,往往要字段排序等。 - -```csharp -class SortedFormContentAttribute : FormContentAttribute -{ - protected override IEnumerable SerializeToKeyValues(ApiParameterContext context) - { - 这里可以排序、加上其它衍生字段等 - return base.SerializeToKeyValues(context).OrderBy(item => item.Key); - } -} - -public interface IDeformedApi -{ - [HttpGet("/path")] - Task PostAsync([SortedFormContent] Model model); -} -``` diff --git a/docs/guide/diy-request-response.md b/docs/guide/diy-request-response.md deleted file mode 100644 index bf92c93a..00000000 --- a/docs/guide/diy-request-response.md +++ /dev/null @@ -1,59 +0,0 @@ - -# 自定义请求内容与响应内容解析 - -除了常见的xml或json响应内容要反序列化为强类型结果模型,你可能会遇到其它的二进制协议响应内容,比如google的ProtoBuf二进制内容。 - -## 1 编写相关自定义特性 - -### 自定义请求内容处理特性 - -```csharp -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; - } -} -``` - -### 自定义响应内容解析特性 - -```csharp -public class ProtobufReturnAttribute : ApiReturnAttribute -{ - public ProtobufReturnAttribute(string acceptContentType = "application/x-protobuf") - : base(new MediaTypeWithQualityHeaderValue(acceptContentType)) - { - } - - public override async Task SetResultAsync(ApiResponseContext context) - { - var stream = await context.HttpContext.ResponseMessage.Content.ReadAsStreamAsync(); - context.Result = Serializer.NonGeneric.Deserialize(context.ApiAction.Return.DataType.Type, stream); - } -} -``` - -## 2 应用相关自定义特性 - -```csharp -[ProtobufReturn] -public interface IProtobufApi -{ - [HttpPut("/users/{id}")] - Task UpdateAsync([Required, PathQuery] string id, [ProtobufContent] User user); -} -``` diff --git a/docs/guide/dynamic-host.md b/docs/guide/dynamic-host.md deleted file mode 100644 index 3470b308..00000000 --- a/docs/guide/dynamic-host.md +++ /dev/null @@ -1,117 +0,0 @@ - -# 动态Host - -针对大家经常提问的动态Host,提供以下简单的示例供参阅;实现的方式不仅限于示例中提及的,**原则上在请求还没有发出去之前的任何环节,都可以修改请求消息的RequestUri来实现动态目标的目的** - -```csharp - - -[LoggingFilter] - public interface IDynamicHostDemo - { - //直接传入绝对目标的方式 - [HttpGet] - ITask ByUrlString([Uri] string urlString); - - //通过Filter的形式 - [HttpGet] - [UriFilter] - ITask ByFilter(); - //通过Attribute修饰的方式 - [HttpGet] - [ServiceName("baiduService")] - ITask ByAttribute(); - } - - /*通过Attribute修饰的方式*/ - - /// - /// 表示对应的服务名 - /// - public class ServiceNameAttribute : ApiActionAttribute - { - public ServiceNameAttribute(string name) - { - Name = name; - OrderIndex = int.MinValue; - } - - public string Name { get; set; } - - public override async Task OnRequestAsync(ApiRequestContext context) - { - await Task.CompletedTask; - IServiceProvider sp = context.HttpContext.ServiceProvider; - HostProvider hostProvider = sp.GetRequiredService(); - //服务名也可以在接口配置时挂在Properties中 - string host = hostProvider.ResolveService(this.Name); - HttpApiRequestMessage requestMessage = context.HttpContext.RequestMessage; - //和原有的Uri组合并覆盖原有Uri - //并非一定要这样实现,只要覆盖了RequestUri,即完成了替换 - requestMessage.RequestUri = requestMessage.MakeRequestUri(new Uri(host)); - } - - } - - /*通过Filter修饰的方式*/ - - /// - ///用来处理动态Uri的拦截器 - /// - public class UriFilterAttribute : ApiFilterAttribute - { - public override Task OnRequestAsync(ApiRequestContext context) - { - var options = context.HttpContext.HttpApiOptions; - //获取注册时为服务配置的服务名 - options.Properties.TryGetValue("serviceName", out object serviceNameObj); - string serviceName = serviceNameObj as string; - IServiceProvider sp = context.HttpContext.ServiceProvider; - HostProvider hostProvider = sp.GetRequiredService(); - string host = hostProvider.ResolveService(serviceName); - HttpApiRequestMessage requestMessage = context.HttpContext.RequestMessage; - //和原有的Uri组合并覆盖原有Uri - //并非一定要这样实现,只要覆盖了RequestUri,即完成了替换 - requestMessage.RequestUri = requestMessage.MakeRequestUri(new Uri(host)); - return Task.CompletedTask; - } - - public override Task OnResponseAsync(ApiResponseContext context) - { - //不处理响应的信息 - return Task.CompletedTask; - } - } - - //以上代码中涉及的依赖项 - public interface IDBProvider - { - string SelectServiceUri(string serviceName); - } - public class DBProvider : IDBProvider - { - public string SelectServiceUri(string serviceName) - { - if (serviceName == "baiduService") return "https://www.baidu.com"; - if (serviceName == "microsoftService") return "https://www.microsoft.com"; - return string.Empty; - } - } - - public class HostProvider - { - private readonly IDBProvider dbProvider; - - public HostProvider(IDBProvider dbProvider) - { - this.dbProvider = dbProvider; - //将HostProvider放到依赖注入容器中,即可从容器获取其它服务来实现动态的服务地址查询 - } - - internal string ResolveService(string name) - { - //如何获取动态的服务地址由你自己决定,此处仅以简单的接口定义简要说明 - return dbProvider.SelectServiceUri(name); - } - } -``` diff --git a/docs/guide/exception-process.md b/docs/guide/exception-process.md deleted file mode 100644 index 4ac61f41..00000000 --- a/docs/guide/exception-process.md +++ /dev/null @@ -1,37 +0,0 @@ - -# 异常和异常处理 - -请求一个接口,不管出现何种异常,最终都抛出HttpRequestException,HttpRequestException的内部异常为实际具体异常,之所以设计为内部异常,是为了完好的保存内部异常的堆栈信息。 - -WebApiClient内部的很多异常都基于ApiException这个抽象异常,也就是很多情况下,抛出的异常都是内为某个ApiException的HttpRequestException。 - -```csharp -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) -{ - // 异常 -} -``` diff --git a/docs/guide/file-download.md b/docs/guide/file-download.md deleted file mode 100644 index 65fc0852..00000000 --- a/docs/guide/file-download.md +++ /dev/null @@ -1,17 +0,0 @@ -# 文件下载 - -```csharp -public interface IUserApi -{ - [HttpGet("/files/{fileName}"] - Task DownloadAsync(string fileName); -} -``` - -```csharp -using System.Net.Http - -var response = await userApi.DownloadAsync('123.zip'); -using var fileStream = File.OpenWrite("123.zip"); -await response.SaveAsAsync(fileStream); -``` diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md deleted file mode 100644 index 7b23f256..00000000 --- a/docs/guide/getting-started.md +++ /dev/null @@ -1,153 +0,0 @@ -# 快速上手 - -::: warning - -+ 如果你的项目所运行的.NET版本支持`.NET Standard2.1`,并具备依赖注入的环境,我们强烈建议你直接使用全新的`WebApiclientCore` -+ `WebApiClient.JIT`、`WebApiClient.AOT` 目前处于 `修补性维护` 阶段。你仍可用用它来构建项目,但我们仅修补致命性错误而不会为其带来任何功能性的更新。 - -::: - -## 依赖环境 - - 对于`WebApiclientCore`,由于基于`.NET Standard2.1`它可以运行在以下平台 - -+ .NET Core 3 + -+ .NET 5、6、7、8 -+ Mono 6.4 + -+ Xamarin.iOS 12.16 + -+ Xamarin.Mac 5.16 + -+ Xamarin.Android 10 + -+ 包括但不限于以上列举的实现`.NET Standard2.1`的平台 - - 对于`WebApiClient.JIT`、`WebApiClient.AOT`,由于基于`.NET Standard2.0`它可以运行在以下平台 - -+ .NET Framework 4.6.1+ -+ .NET Core 2 + -+ .NET Core 3 + -+ .NET 5、6、7、8 -+ Mono 4.6 + -+ Xamarin.iOS 10 + -+ Xamarin.Mac 3 + -+ Xamarin.Android 7 + -+ 通用Windows平台10 + -+ 包括但不限于以上列举的实现`.NET Standard2.0`的平台 -+ 额外支持.NET Framework 4.5 - -## 从Nuget安装 - -这一章节会帮助你从头搭建一个简单的 VuePress 文档网站。如果你想在一个现有项目中使用 VuePress 管理文档,从步骤 3 开始。 - - - - - -```bash -dotnet add package WebApiClientCore -``` - - - - - -```bash -NuGet\Install-Package WebApiClientCore -``` - - - - - -```xml - -``` - - - - -```bash -paket add WebApiClientCore -``` - - - - -## 声明接口 - -```csharp -[HttpHost("http://localhost:5000/")] -public interface IUserApi -{ - [HttpGet("api/users/{id}")] - Task GetAsync(string id); - - [HttpPost("api/users")] - Task PostAsync([JsonContent] User user); -} -``` - -## 注册接口 - -AspNetCore Startup - -```csharp -public void ConfigureServices(IServiceCollection services) -{ - //注册 - services.AddHttpApi(); -} -``` - -Console - -```csharp -public static void Main(string[] args) -{ - //无依赖注入的环境需要自行创建 - IServiceCollection services = new ServiceCollection(); - services.AddHttpApi(); -} -``` - -## 配置 - -```csharp -public void ConfigureServices(IServiceCollection services) -{ - // 注册并配置 - services.AddHttpApi(typeof(IUserApi), o => - { - o.UseLogging = Environment.IsDevelopment(); - o.HttpHost = new Uri("http://localhost:5000/"); - }); - //注册,然后配置 - services.AddHttpApi().ConfigureHttpApi(o => - { - o.UseLogging = Environment.IsDevelopment(); - o.HttpHost = new Uri("http://localhost:5000/"); - }); - //添加全局配置 - services.AddWebApiClient().ConfigureHttpApi(o => - { - o.UseLogging = Environment.IsDevelopment(); - o.HttpHost = new Uri("http://localhost:5000/"); - }); -} -``` - -## 注入接口 - -```csharp -public class MyService -{ - private readonly IUserApi userApi; - public MyService(IUserApi userApi) - { - this.userApi = userApi; - } - - public async Task GetAsync(){ - //使用接口 - var user=await userApi.GetAsync(100); - } -} -``` diff --git a/docs/guide/http-message-handler.md b/docs/guide/http-message-handler.md deleted file mode 100644 index f73327be..00000000 --- a/docs/guide/http-message-handler.md +++ /dev/null @@ -1,117 +0,0 @@ - -# HttpMessageHandler配置 - -## Http代理配置 - -```csharp -services - .AddHttpApi(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双向验证,只允许它执有它颁发的证书的客户端进行连接 - -```csharp -services - .AddHttpApi(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分钟的生命周期。 - -```csharp -var cookieContainer = new CookieContainer(); -services - .AddHttpApi(o => - { - o.HttpHost = new Uri("http://localhost:6000/"); - }) - .ConfigurePrimaryHttpMessageHandler(() => - { - var handler = new HttpClientHandler(); - handler.CookieContainer = cookieContainer; - return handler; - }); -``` - -## Cookie过期自动刷新 - -对于使用Cookie机制的接口,只有在接口请求之后,才知道Cookie是否已失效。通过自定义CookieAuthorizationHandler,可以做在请求某个接口过程中,遇到Cookie失效时自动刷新Cookie再重试请求接口。 - -首先,我们需要把登录接口与某它业务接口拆分在不同的接口定义,例如IUserApi和IUserLoginApi - -```csharp -[HttpHost("http://localhost:5000/")] -public interface IUserLoginApi -{ - [HttpPost("/users")] - Task LoginAsync([JsonContent] Account account); -} -``` - -然后实现自动登录的CookieAuthorizationHandler - -```csharp -public class AutoRefreshCookieHandler : CookieAuthorizationHandler -{ - private readonly IUserLoginApi api; - - public AutoRefreshCookieHandler(IUserLoginApi api) - { - this.api = api; - } - - /// - /// 登录并刷新Cookie - /// - /// 返回登录响应消息 - protected override Task RefreshCookieAsync() - { - return this.api.LoginAsync(new Account - { - account = "admin", - password = "123456" - }); - } -} -``` - -最后,注册IUserApi、IUserLoginApi,并为IUserApi配置AutoRefreshCookieHandler - -```csharp -services - .AddHttpApi(); - -services - .AddHttpApi() - .AddHttpMessageHandler(s => new AutoRefreshCookieHandler(s.GetService())); -``` - -现在,调用IUserApi的任意接口,只要响应的状态码为401,就触发IUserLoginApi登录,然后将登录得到的cookie来重试请求接口,最终响应为正确的结果。你也可以重写CookieAuthorizationHandler的IsUnauthorizedAsync(HttpResponseMessage)方法来指示响应是未授权状态。 diff --git a/docs/guide/interface-demo.md b/docs/guide/interface-demo.md deleted file mode 100644 index d91b984b..00000000 --- a/docs/guide/interface-demo.md +++ /dev/null @@ -1,91 +0,0 @@ -# 接口声明示例 - -这个OpenApi文档在[petstore.swagger.io](https://petstore.swagger.io/),代码为使用WebApiClientCore.OpenApi.SourceGenerator工具将其OpenApi文档反向生成得到 - -```csharp -/// -/// Everything about your Pets -/// -[LoggingFilter] -[HttpHost("https://petstore.swagger.io/v2/")] -public interface IPetApi : IHttpApi -{ - /// - /// Add a new pet to the store - /// - /// Pet object that needs to be added to the store - /// cancellationToken - /// - [HttpPost("pet")] - Task AddPetAsync([Required] [JsonContent] Pet body, CancellationToken cancellationToken = default); - - /// - /// Update an existing pet - /// - /// Pet object that needs to be added to the store - /// cancellationToken - /// - [HttpPut("pet")] - Task UpdatePetAsync([Required] [JsonContent] Pet body, CancellationToken cancellationToken = default); - - /// - /// Finds Pets by status - /// - /// Status values that need to be considered for filter - /// cancellationToken - /// successful operation - [HttpGet("pet/findByStatus")] - ITask> FindPetsByStatusAsync([Required] IEnumerable status, CancellationToken cancellationToken = default); - - /// - /// Finds Pets by tags - /// - /// Tags to filter by - /// cancellationToken - /// successful operation - [Obsolete] - [HttpGet("pet/findByTags")] - ITask> FindPetsByTagsAsync([Required] IEnumerable tags, CancellationToken cancellationToken = default); - - /// - /// Find pet by ID - /// - /// ID of pet to return - /// cancellationToken - /// successful operation - [HttpGet("pet/{petId}")] - ITask GetPetByIdAsync([Required] long petId, CancellationToken cancellationToken = default); - - /// - /// Updates a pet in the store with form data - /// - /// ID of pet that needs to be updated - /// Updated name of the pet - /// Updated status of the pet - /// cancellationToken - /// - [HttpPost("pet/{petId}")] - Task UpdatePetWithFormAsync([Required] long petId, [FormField] string name, [FormField] string status, CancellationToken cancellationToken = default); - - /// - /// Deletes a pet - /// - /// - /// Pet id to delete - /// cancellationToken - /// - [HttpDelete("pet/{petId}")] - Task DeletePetAsync([Header("api_key")] string api_key, [Required] long petId, CancellationToken cancellationToken = default); - - /// - /// uploads an image - /// - /// ID of pet to update - /// Additional data to pass to server - /// file to upload - /// cancellationToken - /// successful operation - [HttpPost("pet/{petId}/uploadImage")] - ITask UploadFileAsync([Required] long petId, [FormDataText] string additionalMetadata, FormDataFile file, CancellationToken cancellationToken = default); -} -``` diff --git a/docs/guide/jsonnet.md b/docs/guide/jsonnet.md deleted file mode 100644 index 054bdad1..00000000 --- a/docs/guide/jsonnet.md +++ /dev/null @@ -1,36 +0,0 @@ - -# NewtonsoftJson处理json - -不可否认,System.Text.Json由于性能的优势,会越来越得到广泛使用,但NewtonsoftJson也不会因此而退出舞台。 - -System.Text.Json在默认情况下十分严格,避免代表调用方进行任何猜测或解释,强调确定性行为,该库是为了实现性能和安全性而特意这样设计的。Newtonsoft.Json默认情况下十分灵活,默认的配置下,你几乎不会遇到反序列化的种种问题,虽然这些问题很多情况下是由于不严谨的json结构或类型声明造成的。 - -## 扩展包 - -默认的基础包是不包含NewtonsoftJson功能的,需要额外引用WebApiClientCore.Extensions.NewtonsoftJson这个扩展包。 - -## 配置[可选] - -```csharp -// ConfigureNewtonsoftJson -services.AddHttpApi().ConfigureNewtonsoftJson(o => -{ - o.JsonSerializeOptions.NullValueHandling = NullValueHandling.Ignore; -}); -``` - -## 声明特性 - -使用[JsonNetReturn]替换内置的[JsonReturn],[JsonNetContent]替换内置[JsonContent] - -```csharp -/// -/// 用户操作接口 -/// -[JsonNetReturn] -public interface IUserApi -{ - [HttpPost("/users")] - Task PostAsync([JsonNetContent] User user); -} -``` diff --git a/docs/guide/log.md b/docs/guide/log.md deleted file mode 100644 index 9bbb9e56..00000000 --- a/docs/guide/log.md +++ /dev/null @@ -1,39 +0,0 @@ -# 日志 - -## 请求和响应日志 - -在整个Interface或某个Method上声明`[LoggingFilter]`,即可把请求和响应的内容输出到LoggingFactory中。如果要排除某个Method不打印日志,在该Method上声明`[LoggingFilter(Enable = false)]`,即可将本Method排除。 - -## 默认日志 - -```csharp -[LoggingFilter] -public interface IUserApi -{ - [HttpGet("api/users/{account}")] - ITask GetAsync([Required]string account); - - // 禁用日志 - [LoggingFilter(Enable =false)] - [HttpPost("api/users/body")] - Task PostByJsonAsync([Required, JsonContent]User user, CancellationToken token = default); -} -``` - -## 自定义日志输出目标 - -```csharp -class MyLoggingAttribute : LoggingFilterAttribute -{ - protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage) - { - xxlogger.Log(logMessage.ToIndentedString(spaceCount: 4)); - return Task.CompletedTask; - } -} - -[MyLogging] -public interface IUserApi -{ -} -``` diff --git a/docs/guide/oauths-token.md b/docs/guide/oauths-token.md deleted file mode 100644 index 076954d3..00000000 --- a/docs/guide/oauths-token.md +++ /dev/null @@ -1,200 +0,0 @@ - -# 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 - -```csharp -// 为接口注册与配置Client模式的tokenProvider -services.AddClientCredentialsTokenProvider(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会得到一次失败的请求,本次失败的请求无法避免。 - -```csharp -/// -/// 用户操作接口 -/// -[OAuthToken] -public interface IUserApi -{ - ... -} -``` - -OAuthTokenAttribute默认实现将token放到Authorization请求头,如果你的接口需要请token放到其它地方比如uri的query,需要重写OAuthTokenAttribute: - -```csharp -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对应的业务模型的某个标记字段,也是非常棘手的活。 - -```csharp -// 注册接口时添加OAuthTokenHandler -services - .AddHttpApi() - .AddOAuthTokenHandler(); -``` - -OAuthTokenHandler默认实现将token放到Authorization请求头,如果你的接口需要请token放到其它地方比如uri的query,需要重写OAuthTokenHandler: - -```csharp -class UriQueryOAuthTokenHandler : OAuthTokenHandler -{ - /// - /// token应用的http消息处理程序 - /// - /// token提供者 - public UriQueryOAuthTokenHandler(ITokenProvider tokenProvider) - : base(tokenProvider) - { - } - - /// - /// 应用token - /// - /// - /// - 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; - - var uriValue = new UriValue(request.RequestUri).AddQuery("myToken", tokenResult.Access_token); - request.RequestUri = uriValue.ToUri(); - } -} - - -// 注册接口时添加UriQueryOAuthTokenHandler -services - .AddHttpApi() - .AddOAuthTokenHandler((s, tp) => new UriQueryOAuthTokenHandler(tp)); -``` - -## 多接口共享的TokenProvider - -可以给http接口设置基础接口,然后为基础接口配置TokenProvider,例如下面的xxx和yyy接口,都属于IBaidu,只需要给IBaidu配置TokenProvider。 - -```csharp -[OAuthToken] -public interface IBaidu -{ -} - -public interface IBaidu_XXX_Api : IBaidu -{ - [HttpGet] - Task xxxAsync(); -} - -public interface IBaidu_YYY_Api : IBaidu -{ - [HttpGet] - Task yyyAsync(); -} -``` - -```csharp -// 注册与配置password模式的token提者选项 -services.AddPasswordCredentialsTokenProvider(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的接口如下: - -```csharp -public interface ITokenApi -{ - [HttpPost("http://xxx.com/token")] - Task RequestTokenAsync([Parameter(Kind.Form)] string clientId, [Parameter(Kind.Form)] string clientSecret); -} -``` - -### 委托TokenProvider - -委托TokenProvider是一种最简单的实现方式,它将请求token的委托作为自定义TokenProvider的实现逻辑: - -```csharp -// 为接口注册自定义tokenProvider -services.AddTokeProvider(s => -{ - return s.GetService().RequestTokenAsync("id", "secret"); -}); -``` - -### 完整实现的TokenProvider - -```csharp -// 为接口注册CustomTokenProvider -services.AddTokeProvider(); -``` - -```csharp -class CustomTokenProvider : TokenProvider -{ - public CustomTokenProvider(IServiceProvider serviceProvider) - : base(serviceProvider) - { - } - - protected override Task RequestTokenAsync(IServiceProvider serviceProvider) - { - return serviceProvider.GetService().RequestTokenAsync("id", "secret"); - } - - protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) - { - return this.RequestTokenAsync(serviceProvider); - } -} -``` - -### 自定义TokenProvider的选项 - -每个TokenProvider都有一个Name属性,与service.AddTokeProvider()返回的ITokenProviderBuilder的Name是同一个值。读取Options值可以使用TokenProvider的GetOptionsValue()方法,配置Options则通过ITokenProviderBuilder的Name来配置。 diff --git a/docs/guide/readme.md b/docs/guide/readme.md index b9e2308b..5edd7296 100644 --- a/docs/guide/readme.md +++ b/docs/guide/readme.md @@ -10,18 +10,14 @@ ## 简介 -WebApiClient有两个版本 +WebApiClient 有两个版本 -+ `WebApiclientCore` 基于`.NET Standard2.1`重新设计的新版本,与全新的`依赖注入`、`配置`、`选项`、`日志`等重新设计过的.NET抽象Api完美契合 -+ `WebApiClient.JIT`、`WebApiClient.AOT` 基于`.NET Standard2.0`的旧版本(额外支持`.NET Framework 4.5+`),支持`.NET Core 2.0+`,在老版本的.NET上亦能独当一面 -+ [QQ群 825135345]()进群时请注明**WebApiClient**,在咨询问题之前,请先认真阅读以下剩余的文档,避免消耗作者不必要的重复解答时间。 -+ 反馈问题请前往 [https://github.com/dotnetcore/WebApiClient/issues](https://github.com/dotnetcore/WebApiClient/issues) +### WebApiclientCore +`WebApiclientCore` 基于`.NET Standard2.1`重新设计的新版本,与全新的`依赖注入`、`配置`、`选项`、`日志`等重新设计过的.NET 抽象 Api 完美契合,欢迎您使用、提问、贡献代码、提供创意。 +### WebApiClient +`WebApiClient.JIT`、`WebApiClient.AOT` 基于`.NET Standard2.0`的旧版本(额外支持`.NET Framework 4.5+`),支持`.NET Core 2.0+`,在老版本的.NET 上亦能独当一面,但我们不会继续更新它。 -## 特性 - -+ 支持编译时代理类生成包,提高运行时性能和兼容性 -+ 支持OAuth2与token管理扩展包,方便实现身份认证和授权 -+ 支持Json.Net扩展包,提供灵活的Json序列化和反序列化 -+ 支持JsonRpc调用扩展包,支持使用JsonRpc协议进行远程过程调用 -+ 支持将本地或远程OpenApi文档解析生成WebApiClientCore接口代码的dotnet tool,简化接口声明的工作量 -+ 提供接口声明的语法分析与提示,帮助开发者避免使用不当的语法 +## 服务渠道 +1. QQ 群 [825135345](https://shang.qq.com/wpa/qunwpa?idkey=c6df21787c9a774ca7504a954402c9f62b6595d1e63120eabebd6b2b93007410) 进群时请注明 **WebApiClient** + +2. 反馈问题请前往 [https://github.com/dotnetcore/WebApiClient/issues](https://github.com/dotnetcore/WebApiClient/issues) \ No newline at end of file diff --git a/docs/guide/request.md b/docs/guide/request.md deleted file mode 100644 index a6a15066..00000000 --- a/docs/guide/request.md +++ /dev/null @@ -1,179 +0,0 @@ -# 请求声明 - -## 表单集合处理 - -按照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。 - -```csharp -[HttpGet("api/users/{id}")] -ITask 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。 - -## PATCH请求 - -json patch是为客户端能够局部更新服务端已存在的资源而设计的一种标准交互,在RFC6902里有详细的介绍json patch,通俗来讲有以下几个要点: - -1. 使用HTTP PATCH请求方法; -2. 请求body为描述多个opration的数据json内容; -3. 请求的Content-Type为application/json-patch+json; - -### 声明Patch方法 - -```csharp -public interface IUserApi -{ - [HttpPatch("api/users/{id}")] - Task PatchAsync(string id, JsonPatchDocument doc); -} -``` - -### 实例化JsonPatchDocument - -```csharp -var doc = new JsonPatchDocument(); -doc.Replace(item => item.Account, "laojiu"); -doc.Replace(item => item.Email, "laojiu@qq.com"); -``` - -### 请求内容 - -```csharp -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"}] -``` - -## 非模型请求 - -有时候我们未必需要强模型,假设我们已经有原始的form文本内容,或原始的json文本内容,甚至是System.Net.Http.HttpContent对象,只需要把这些原始内请求到远程远程器。 - -### 原始文本 - -```csharp -[HttpPost] -Task PostAsync([RawStringContent("txt/plain")] string text); - -[HttpPost] -Task PostAsync(StringContent text); -``` - -### 原始json - -```csharp -[HttpPost] -Task PostAsync([RawJsonContent] string json); -``` - -### 原始xml - -```csharp -[HttpPost] -Task PostAsync([RawXmlContent] string xml); -``` - -### 原始表单内容 - -```csharp -[HttpPost] -Task PostAsync([RawFormContent] string form); -``` - -## 自定义自解释的参数类型 - -在某些极限情况下,比如人脸比对的接口,我们输入模型与传输模型未必是对等的,例如: - -服务端要求的json模型 - -```json -{ - "image1" : "图片1的base64", - "image2" : "图片2的base64" -} -``` - -客户端期望的业务模型 - -```csharp -class FaceModel -{ - public Bitmap Image1 {get; set;} - public Bitmap Image2 {get; set;} -} -``` - -我们希望构造模型实例时传入Bitmap对象,但传输的时候变成Bitmap的base64值,所以我们要改造FaceModel,让它实现IApiParameter接口: - -```csharp -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 options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = new 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来请求 - -```csharp -public interface IFaceApi -{ - [HttpPost("/somePath")] - Task PostAsync(FaceModel faces); -} -``` diff --git a/docs/guide/response.md b/docs/guide/response.md deleted file mode 100644 index c0d49b4c..00000000 --- a/docs/guide/response.md +++ /dev/null @@ -1,89 +0,0 @@ -# 响应处理 - -## 缺省配置值 - -缺省配置是[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` - -## 原始类型返回值 - -当接口返回值声明为如下类型时,我们称之为原始类型,会被RawReturnAttribute处理。 - -返回类型 | 说明 ----|--- -`Task` | 不关注响应消息 -`Task` | 原始响应消息类型 -`Task` | 原始响应流 -`Task` | 原始响应二进制数据 -`Task` | 原始响应消息文本 - -## 响应内容缓存 - -配置CacheAttribute特性的Method会将本次的响应内容缓存起来,下一次如果符合预期条件的话,就不会再请求到远程服务器,而是从IResponseCacheProvider获取缓存内容,开发者可以自己实现ResponseCacheProvider。 - -### 声明缓存特性 - -```csharp -public interface IUserApi -{ - // 缓存一分钟 - [Cache(60 * 1000)] - [HttpGet("api/users/{account}")] - ITask GetAsync([Required]string account); -} -``` - -默认缓存条件:URL(如`http://abc.com/a`)和指定的请求Header一致。 -如果需要类似`[CacheByPath]`这样的功能,可直接继承`ApiCacheAttribute`来实现: - -```csharp - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class CacheByAbsolutePathAttribute : ApiCacheAttribute - { - public CacheByPathAttribute(double expiration) : base(expiration) - { - } - - public override Task GetCacheKeyAsync(ApiRequestContext context) - { - return Task.FromResult(context.HttpContext.RequestMessage.RequestUri.AbsolutePath); - } - } -``` - -### 自定义缓存提供者 - -默认的缓存提供者为内存缓存,如果希望将缓存保存到其它存储位置,则需要自定义 缓存提者,并注册替换默认的缓存提供者。 - -```csharp -public class RedisResponseCacheProvider : IResponseCacheProvider -{ - public string Name => nameof(RedisResponseCacheProvider); - - public Task GetAsync(string key) - { - throw new NotImplementedException(); - } - - public Task SetAsync(string key, ResponseCacheEntry entry, TimeSpan expiration) - { - throw new NotImplementedException(); - } -} -``` - -```csharp -public static IWebApiClientBuilder UseRedisResponseCacheProvider(this IWebApiClientBuilder builder) -{ - builder.Services.AddSingleton(); - return builder; -} -``` diff --git a/docs/guide/retry.md b/docs/guide/retry.md deleted file mode 100644 index 07930c7e..00000000 --- a/docs/guide/retry.md +++ /dev/null @@ -1,16 +0,0 @@ -# 请求条件性重试 - -使用ITask<>异步声明,就有Retry的扩展,Retry的条件可以为捕获到某种Exception或响应模型符合某种条件。 - -```csharp -public interface IUserApi -{ - [HttpGet("api/users/{id}")] - ITask GetAsync(string id); -} - -var result = await userApi.GetAsync(id: "id001") - .Retry(maxCount: 3) - .WhenCatch() - .WhenResult(r => r.Age <= 0); -``` diff --git a/docs/guide/source-generator.md b/docs/guide/source-generator.md deleted file mode 100644 index 7dba804d..00000000 --- a/docs/guide/source-generator.md +++ /dev/null @@ -1,13 +0,0 @@ -# SourceGenerator - -SourceGenerator是一种新的c#编译时代码补充生成技术,可以非常方便的为WebApiClient生成接口的代理实现类,使用SourceGenerator扩展包,可以替换默认的内置Emit生成代理类的方案,支持需要完全AOT编译的平台。 - -引用WebApiClientCore.Extensions.SourceGenerator,并在项目启用如下配置: - -```csharp -// 应用编译时生成接口的代理类型代码 -services - .AddWebApiClient() - .UseSourceGeneratorHttpApiActivator() - .AddDynamicDependency{AssemblyName}; -``` diff --git a/docs/guide/url-rule.md b/docs/guide/url-rule.md deleted file mode 100644 index e06b10ce..00000000 --- a/docs/guide/url-rule.md +++ /dev/null @@ -1,18 +0,0 @@ - -# 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的结尾的第一种方式。 diff --git a/docs/old/advanced/env-with-di.md b/docs/old/advanced/env-with-di.md index 71d029e4..8766ebb2 100644 --- a/docs/old/advanced/env-with-di.md +++ b/docs/old/advanced/env-with-di.md @@ -1,6 +1,6 @@ -# 2、有依赖注入的环境 +# 有依赖注入的环境 -## 2.1 Asp.net core 2.1+ +## Asp.net core 2.1+ 接口声明 @@ -38,7 +38,7 @@ public class HomeController : Controller } ``` -## 2.2 Asp.net MVC + Autofac +## Asp.net MVC + Autofac 接口声明 diff --git a/docs/old/advanced/env-without-di.md b/docs/old/advanced/env-without-di.md index d4b09cd6..e007a3a5 100644 --- a/docs/old/advanced/env-without-di.md +++ b/docs/old/advanced/env-without-di.md @@ -1,6 +1,6 @@ -# 1、没有依赖注入的环境 +# 没有依赖注入的环境 -## 1.1 使用HttpApi.Register/Resolve +## 使用HttpApi.Register/Resolve 接口声明 diff --git a/docs/old/basic/attribute-scope-features.md b/docs/old/basic/attribute-scope-features.md index 4927300e..7b953023 100644 --- a/docs/old/basic/attribute-scope-features.md +++ b/docs/old/basic/attribute-scope-features.md @@ -1,6 +1,6 @@ -# 7、特性的范围和优先级 +# 特性的范围和优先级 -## 7.1 特性的范围 +## 特性的范围 有些特性比如`[Header]`,可以修饰于接口、方法和参数,使用不同的构造器和修饰于不同的地方产生的含义和结果是有点差别的: @@ -8,7 +8,7 @@ 修饰方法时,表示此方法在请求前添加这个请求头; 修饰参数时,表示参数的值将做为请求头的值,由调用者动态传入; -## 7.2 特性的优先级 +## 特性的优先级 方法级比接口级优先级高; `AllowMultiple`为`true`时,方法级和接口级都生效; diff --git a/docs/old/basic/full-demo.md b/docs/old/basic/full-demo.md index 9292d595..32296c00 100644 --- a/docs/old/basic/full-demo.md +++ b/docs/old/basic/full-demo.md @@ -1,8 +1,8 @@ -# 8、完整接口声明示例 +# 完整接口声明示例 本示例的接口为swagger官方的v2/swagger.json,参见swagger官网接口,对于swagger文档,可以使用WebApiClient.Tools.Swagger工具生成客户端代码。 -## 8.1 IPetApi +## IPetApi ```csharp using System; @@ -95,7 +95,7 @@ namespace petstore.swagger } ``` -## 8.2 IUserApi +## IUserApi ```csharp using System; diff --git a/docs/old/basic/get-head.md b/docs/old/basic/get-head.md index 75077343..f33b5b3c 100644 --- a/docs/old/basic/get-head.md +++ b/docs/old/basic/get-head.md @@ -1,6 +1,6 @@ -# 1、GET/HEAD请求 +# GET/HEAD请求 -## 1.1 Get请求简单例子 +## Get请求简单例子 ```csharp public interface IMyWebApi : IHttpApi @@ -14,7 +14,7 @@ var api = HttpApi.Create(); var response = await api.GetUserByAccountAsync("laojiu"); ``` -## 1.2 使用`[HttpHost]`特性 +## 使用`[HttpHost]`特性 ```csharp @@ -29,9 +29,9 @@ public interface IMyWebApi : IHttpApi 如果接口IMyWebApi有多个方法且都指向同一服务器,可以将请求的域名抽出来放到HttpHost特性。 -## 1.3 响应的json/xml内容转换为强类型模型 +## 响应的json/xml内容转换为强类型模型 -### 1.3.1 隐式转换为强类型模型 +### 隐式转换为强类型模型 ```csharp @@ -46,7 +46,7 @@ public interface IMyWebApi : IHttpApi 当方法的返回数据是UserInfo类型的json或xml文本,且响应的Content-Type为application/json或application/xml值时,方法的原有返回类型ITask(Of HttpResponseMessage)就可以声明为ITask(Of UserInfo)。 -### 1.3.2 显式转换为强类型模型 +### 显式转换为强类型模型 ```csharp [HttpHost("http://www.mywebapi.com/")] @@ -61,7 +61,7 @@ public interface IMyWebApi : IHttpApi 当方法的返回数据是UserInfo类型的json或xml文本,但响应的Content-Type可能不是期望的application/json或application/xml值时,就需要显式声明[JsonReturn]或[XmlReturn]特性。 -## 1.4 请求取消令牌参数CancellationToken +## 请求取消令牌参数CancellationToken ```csharp [HttpHost("http://www.mywebapi.com/")] diff --git a/docs/old/basic/parameter-attribute.md b/docs/old/basic/parameter-attribute.md index 36eb8d76..20cf7ba3 100644 --- a/docs/old/basic/parameter-attribute.md +++ b/docs/old/basic/parameter-attribute.md @@ -1,8 +1,8 @@ -# 5、参数及属性注解 +# 参数及属性注解 这些注解特性的命名空间在WebApiClient.DataAnnotations,用于影响参数的序列化行为。 -## 5.1 参数别名 +## 参数别名 ```csharp public interface IMyWebApi : IHttpApi @@ -14,7 +14,7 @@ public interface IMyWebApi : IHttpApi } ``` -## 5.2 参数模型属性注解 +## 参数模型属性注解 ```csharp public class UserInfo diff --git a/docs/old/basic/parameter-validation.md b/docs/old/basic/parameter-validation.md index 5baff497..7470b205 100644 --- a/docs/old/basic/parameter-validation.md +++ b/docs/old/basic/parameter-validation.md @@ -1,8 +1,8 @@ -# 6、参数及参数属性输入验证 +# 参数及参数属性输入验证 这些验证特性都有相同的基类ValidationAttribute,命名空间为System.ComponentModel.DataAnnotations,由netfx或corefx提供。 -## 6.1 参数值的验证 +## 参数值的验证 ```csharp [HttpGet("webapi/user/GetById/{id}")] @@ -12,7 +12,7 @@ ITask GetByIdAsync( id的参数要求必填且最大长度为10的字符串,否则抛出ValidationException的异常。 -## 6.2 参数的属性值验证 +## 参数的属性值验证 ```csharp public class UserInfo @@ -33,7 +33,7 @@ ITask UpdateWithJsonAsync( 当user参数不为null的情况,就会验证它的Account和Password两个属性,HttpApiConfig有个UseParameterPropertyValidate属性,设置为false就禁用验证参数的属性值。 -## 6.3 两者同时验证 +## 两者同时验证 对于上节的例子,如果我们希望user参数值也不能为null,可以如下声明方法: diff --git a/docs/old/basic/patch.md b/docs/old/basic/patch.md index 38936269..1ad1a225 100644 --- a/docs/old/basic/patch.md +++ b/docs/old/basic/patch.md @@ -1,4 +1,4 @@ -# 4、PATCH请求 +# PATCH请求 json patch是为客户端能够局部更新服务端已存在的资源而设计的一种标准交互,在RFC6902里有详细的介绍json patch,通俗来讲有以下几个要点: @@ -6,7 +6,7 @@ json patch是为客户端能够局部更新服务端已存在的资源而设计 请求body为描述多个opration的数据json内容; 请求的Content-Type为application/json-patch+json; -## 4.1 WebApiClient例子 +## WebApiClient例子 ```csharp public interface IMyWebApi : IHttpApi @@ -22,7 +22,7 @@ var api = HttpApi.Create(); await api.PatchAsync("id001", doc); ``` -## 4.2 Asp.net 服务端例子 +## Asp.net 服务端例子 ```csharp [HttpPatch] diff --git a/docs/old/basic/post-put-delete.md b/docs/old/basic/post-put-delete.md index a3094d37..46a68f4a 100644 --- a/docs/old/basic/post-put-delete.md +++ b/docs/old/basic/post-put-delete.md @@ -1,6 +1,6 @@ -# 3、POST/PUT/DELETE请求 +# POST/PUT/DELETE请求 -## 3.1 使用Json或Xml提交 +## 使用Json或Xml提交 使用`[XmlContent]`或`[Parameter(Kind.XmlBody)]`修饰强类型模型参数,表示提交xml 使用`[JsonContent]`或`[Parameter(Kind.JsonBody)]`修饰强类型模型参数,表示提交json @@ -17,7 +17,7 @@ ITask AddUserWithJsonAsync([JsonContent] UserInfo user); ITask UpdateUserWithXmlAsync([XmlContent] UserInfo user); ``` -## 3.2 使用x-www-form-urlencoded提交 +## 使用x-www-form-urlencoded提交 使用`[FormContent]`或`[Parameter(Kind.Form)]`修饰强类型模型参数 使用`[FormField]`或`[Parameter(Kind.Form)]`修饰简单类型参数 @@ -44,7 +44,7 @@ ITask UpdateUserAsync( [Parameter(Kind.Form)] string fieldX); ``` -## 3.3 使用multipart/form-data提交 +## 使用multipart/form-data提交 使用`[MulitpartContent]`或`[Parameter(Kind.FormData)]`修饰强类型模型参数 使用`[MulitpartText]`或`[Parameter(Kind.FormData)]`修饰简单类型参数 @@ -71,7 +71,7 @@ ITask UpdateUserWithMulitpartAsync( MulitpartFile file); ``` -## 3.4 使用具体的HttpContent类型提交 +## 使用具体的HttpContent类型提交 ```csharp // POST webapi/user diff --git a/docs/old/basic/request-url.md b/docs/old/basic/request-url.md index 76362cad..bfd0e734 100644 --- a/docs/old/basic/request-url.md +++ b/docs/old/basic/request-url.md @@ -1,12 +1,12 @@ -# 2、请求URI +# 请求URI -## 2.1 URI的格式 +## URI的格式 无论是GET还是POST等哪种http请求方法,都遵循如下的URI格式: {Scheme}://{UserName}:{Password}@{Host}:{Port}{Path}{Query}{Fragment} 例如:`` -## 2.2 动态PATH +## 动态PATH ```csharp public interface IMyWebApi : IHttpApi @@ -19,7 +19,7 @@ public interface IMyWebApi : IHttpApi 某些接口方法将路径的一个分段语意化,比如GET `。 -## 2.3 动态URI +## 动态URI ```csharp public interface IMyWebApi : IHttpApi @@ -36,9 +36,9 @@ public interface IMyWebApi : IHttpApi 如果请求URI在运行时才确定,可以将请求URI作为一个参数,使用`[Uri]`特性修饰这个参数并作为第一个参数。 -## 2.4 Query参数 +## Query参数 -### 2.4.1 多个query参数平铺 +### 多个query参数平铺 ```csharp // GET /webapi/user?account=laojiu&password=123456 @@ -46,7 +46,7 @@ public interface IMyWebApi : IHttpApi ITask GetUserAsync(string account, string password); ``` -### 2.4.2 多个query参数合并到模型 +### 多个query参数合并到模型 ```csharp public class LoginInfo @@ -60,7 +60,7 @@ public class LoginInfo ITask GetUserAsync(LoginInfo loginInfo); ``` -### 2.4.3 多个query参数平铺+部分合并到模型 +### 多个query参数平铺+部分合并到模型 ```csharp public class LoginInfo @@ -74,7 +74,7 @@ public class LoginInfo ITask GetUserAsync(LoginInfo loginInfo, string role); ``` -### 2.4.4 显式声明`[PathQuery]`特性 +### 显式声明`[PathQuery]`特性 ```csharp // GET /webapi/user?account=laojiu&password=123456&role=admin @@ -86,7 +86,7 @@ ITask GetUserAsync( 对于没有任何特性修饰的每个参数,都默认被`[PathQuery]`修饰,表示做为请求路径或请求参数处理,`[PathQuery]`特性可以设置`Encoding`、`IgnoreWhenNull`和`DateTimeFormat`多个属性。 -### 2.4.5 使用`[Parameter(Kind.Query)]`特性 +### 使用`[Parameter(Kind.Query)]`特性 ```csharp // GET /webapi/user?account=laojiu&password=123456&role=admin diff --git a/docs/old/getting-started.md b/docs/old/getting-started.md index 3c27e5d7..19b5f577 100644 --- a/docs/old/getting-started.md +++ b/docs/old/getting-started.md @@ -1,13 +1,13 @@ # 快速开始 -## 1 Nuget包 +## Nuget包 | 包名 | 描述 | Nuget | ---|---|--| | WebApiClient.JIT | 适用于非AOT编译的所有平台,稳定性好 | [![NuGet](https://buildstats.info/nuget/WebApiClient.JIT)](https://www.nuget.org/packages/WebApiClient.JIT) | | WebApiClient.AOT | 适用于所有平台,包括IOS和UWP,复杂依赖项目可能编译不通过 | [![NuGet](https://buildstats.info/nuget/WebApiClient.AOT)](https://www.nuget.org/packages/WebApiClient.AOT) | -## 2. Http请求 +## Http请求 > > 接口的声明 diff --git a/docs/old/qa.md b/docs/old/qa.md index ff7cc8b3..20ad6cbd 100644 --- a/docs/old/qa.md +++ b/docs/old/qa.md @@ -1,33 +1,33 @@ -# Q&A +# 常见问题 -## 1 声明的http接口为什么要继承IHttpApi接口? +## 声明的http接口为什么要继承IHttpApi接口? 一是为了方便WebApiClient库自动生成接口的代理类,相当用于标记作用;二是继承了`IHttpApi`接口,http接口代理类实例就有Dispose方法。 -## 2 http接口可以继承其它http接口吗? +## http接口可以继承其它http接口吗? 可以继承,父接口的相关方法也都当作Api方法,需要注意的是,父接口的方法的接口级特性将失效,而是应用了子接口的接口级特性,所以为了方便理解,最好不要这样继承。 -## 3 使用`[ProxyAttribute(host,port)]`代理特性前,怎么验证代理的有效性? +## 使用`[ProxyAttribute(host,port)]`代理特性前,怎么验证代理的有效性? 可以使用ProxyValidator对象的Validate方法来验证代理的有效性。 -## 4 为什么不支持将接口方法的返回类型声明为`Task`对象而必须为`Task<>`或`ITask<>`? +## 为什么不支持将接口方法的返回类型声明为`Task`对象而必须为`Task<>`或`ITask<>`? 这个是设计的原则,因为不管开发者关不关注返回值,Http请求要么有响应要么抛出异常,如果你不关注结果的解析,可以声明为`Task`而不去解析`HttpResponseMessage`就可以。 -## 5 使用WebApiClient怎么下载文件? +## 使用WebApiClient怎么下载文件? 你应该将接口返回类型声明为`ITask`。 -## 6 接口返回类型除了声明为`ITask`,还可以声明哪些抽象的返回类型? +## 接口返回类型除了声明为`ITask`,还可以声明哪些抽象的返回类型? 还可以声明为`ITask`、`ITask`和`ITask`,这些都是抽象的返回类型。 -## 7 接口声明的参数可以为Object或某些类型的基类吗? +## 接口声明的参数可以为Object或某些类型的基类吗? 可以这样声明,数据还是子类的,但xml序列化会有问题,一般情况下,建议严格按照服务器的具体类型来声明参数。 -## 8 WebApiClient怎么使用同步请求 +## WebApiClient怎么使用同步请求 WebApiClient是对HttpClient的封包,HttpClient没有提供相关的同步请求方法,所以WebApiClient也没有同步请求,不正确的阻塞ITask和Task返回值,在一些环境下很容易死锁。 diff --git a/docs/reference/nuget.md b/docs/reference/nuget.md deleted file mode 100644 index 10781631..00000000 --- a/docs/reference/nuget.md +++ /dev/null @@ -1,10 +0,0 @@ -### Nuget - -| 包名 | 描述 | Nuget | ----|---|--| -| WebApiClientCore | 基础包 | [![NuGet](https://buildstats.info/nuget/WebApiClientCore)](https://www.nuget.org/packages/WebApiClientCore) | -| WebApiClientCore.Extensions.SourceGenerator | 编译时代理类生成包 | [![NuGet](https://buildstats.info/nuget/WebApiClientCore.Extensions.SourceGenerator)](https://www.nuget.org/packages/WebApiClientCore.Extensions.SourceGenerator) | -| WebApiClientCore.Extensions.OAuths | OAuth2与token管理扩展包 | [![NuGet](https://buildstats.info/nuget/WebApiClientCore.Extensions.OAuths)](https://www.nuget.org/packages/WebApiClientCore.Extensions.OAuths) | -| WebApiClientCore.Extensions.NewtonsoftJson | Json.Net扩展包 | [![NuGet](https://buildstats.info/nuget/WebApiClientCore.Extensions.NewtonsoftJson)](https://www.nuget.org/packages/WebApiClientCore.Extensions.NewtonsoftJson) | -| WebApiClientCore.Extensions.JsonRpc | JsonRpc调用扩展包 | [![NuGet](https://buildstats.info/nuget/WebApiClientCore.Extensions.JsonRpc)](https://www.nuget.org/packages/WebApiClientCore.Extensions.JsonRpc) | -| WebApiClientCore.OpenApi.SourceGenerator | 将本地或远程OpenApi文档解析生成WebApiClientCore接口代码的dotnet tool | [![NuGet](https://buildstats.info/nuget/WebApiClientCore.OpenApi.SourceGenerator)](https://www.nuget.org/packages/WebApiClientCore.OpenApi.SourceGenerator) | diff --git a/icon.png b/icon.png index 9f89572d..0bbbc271 100644 Binary files a/icon.png and b/icon.png differ diff --git a/icon.psd b/icon.psd new file mode 100644 index 00000000..f8cec351 Binary files /dev/null and b/icon.psd differ diff --git a/nuget.push.bat b/nuget.push.bat new file mode 100644 index 00000000..1de21c98 --- /dev/null +++ b/nuget.push.bat @@ -0,0 +1,8 @@ +@echo off + +set "folder=artifacts\package\release" + +for %%f in ("%folder%\*.nupkg") do ( + echo push %%f + nuget push %%f -source https://api.nuget.org/v3/index.json +) \ No newline at end of file