From f19c68d52221faa9ef66cfe01835a6f4b56b73a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Wed, 5 Jun 2024 15:09:13 +0800 Subject: [PATCH 001/108] 2.0.9 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 16d7cf4b..0ac339a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.0.8 + 2.0.9 Copyright © laojiu 2017-2024 IDE0057;IDE0290 From 6b52ea44ac3c5cfa2d634ac405f0984f114fb066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 12:39:03 +0800 Subject: [PATCH 002/108] add UseILEmitHttpApiActivator() --- Directory.Build.props | 2 +- .../WebApiClientBuilderExtensions.cs | 22 ----------- ...ientCore.Extensions.SourceGenerator.csproj | 11 ++---- .../HttpApiConfigureExtensions.cs | 16 ++++---- .../WebApiClientBuilderExtensions.cs | 37 +++++++++++++++---- .../DefaultHttpApiActivator.cs | 6 +-- ...Activator.cs => ILEmitHttpApiActivator.cs} | 8 ++-- 7 files changed, 49 insertions(+), 53 deletions(-) delete mode 100644 WebApiClientCore.Extensions.SourceGenerator/WebApiClientBuilderExtensions.cs rename WebApiClientCore/Implementations/{EmitHttpApiActivator.cs => ILEmitHttpApiActivator.cs} (96%) diff --git a/Directory.Build.props b/Directory.Build.props index 0ac339a8..1f3825f9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.0.9 + 2.1.0 Copyright © laojiu 2017-2024 IDE0057;IDE0290 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/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/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/Implementations/DefaultHttpApiActivator.cs b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs index 516df648..38807522 100644 --- a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs @@ -14,7 +14,7 @@ public class DefaultHttpApiActivator< #endif THttpApi> : IHttpApiActivator { - private readonly Lazy> emitHttpApiActivatorLazy; + private readonly Lazy> ilEmitHttpApiActivatorLazy; private readonly SourceGeneratorHttpApiActivator? sourceGeneratorHttpApiActivator; /// @@ -30,7 +30,7 @@ public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorP { this.sourceGeneratorHttpApiActivator = new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); } - this.emitHttpApiActivatorLazy = new Lazy>(() => new EmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider), isThreadSafe: true); + this.ilEmitHttpApiActivatorLazy = new Lazy>(() => new ILEmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider), isThreadSafe: true); } /// @@ -41,7 +41,7 @@ public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorP public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) { return this.sourceGeneratorHttpApiActivator == null - ? this.emitHttpApiActivatorLazy.Value.CreateInstance(apiInterceptor) + ? this.ilEmitHttpApiActivatorLazy.Value.CreateInstance(apiInterceptor) : this.sourceGeneratorHttpApiActivator.CreateInstance(apiInterceptor); } } diff --git a/WebApiClientCore/Implementations/EmitHttpApiActivator.cs b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs similarity index 96% rename from WebApiClientCore/Implementations/EmitHttpApiActivator.cs rename to WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs index 622cfc9a..43108b67 100644 --- a/WebApiClientCore/Implementations/EmitHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs @@ -8,10 +8,10 @@ namespace WebApiClientCore.Implementations { /// - /// 运行时使用Emit动态创建THttpApi的代理类和代理类实例 + /// 运行时使用ILEmit动态创建THttpApi的代理类和代理类实例 /// /// - public class EmitHttpApiActivator< + public class ILEmitHttpApiActivator< #if NET5_0_OR_GREATER [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] #endif @@ -21,13 +21,13 @@ public class EmitHttpApiActivator< private readonly Func activator; /// - /// 运行时使用Emit动态创建THttpApi的代理类和代理类实例 + /// 运行时使用ILEmit动态创建THttpApi的代理类和代理类实例 /// /// /// /// /// - public EmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) + public ILEmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var apiMethods = HttpApi.FindApiMethods(typeof(THttpApi)); From 4fb19fbcd6c9a72eb6ae6b0dc64069c61147b24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 13:18:13 +0800 Subject: [PATCH 003/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Class=E5=90=8E?= =?UTF-8?q?=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 12 +++---- .../HttpApiProxyClassInitializer.cs | 31 ++++++++----------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 876c2289..46d9aca0 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -21,12 +21,12 @@ sealed class HttpApiProxyClass : IEquatable /// /// 拦截器变量名 /// - private readonly string apiInterceptorFieldName = $"apiInterceptor_{(uint)Environment.TickCount}"; + private readonly string apiInterceptorFieldName = "_apiInterceptor"; /// /// action执行器变量名 /// - private readonly string actionInvokersFieldName = $"actionInvokers_{(uint)Environment.TickCount}"; + private readonly string actionInvokersFieldName = "_actionInvokers"; /// /// 文件名 @@ -41,7 +41,7 @@ sealed class HttpApiProxyClass : IEquatable /// /// 类型名 /// - public string ClassName => this.httpApi.Name; + public string ClassName => this.httpApi.Name + "Class"; /// /// HttpApi代理类 @@ -89,13 +89,13 @@ public override string ToString() 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 {this.ClassName} : {this.httpApiFullName}"); 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\tpublic {this.ClassName}(global::WebApiClientCore.IHttpApiInterceptor apiInterceptor, global::WebApiClientCore.ApiActionInvoker[] actionInvokers)"); builder.AppendLine("\t\t{"); builder.AppendLine($"\t\t\tthis.{this.apiInterceptorFieldName} = apiInterceptor;"); builder.AppendLine($"\t\t\tthis.{this.actionInvokersFieldName} = actionInvokers;"); @@ -138,7 +138,7 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, : $"new global::System.Object[] {{ {parameterNamesString} }}"; var methodName = $"\"{interfaceType.ToDisplayString()}.{method.Name}\""; - var returnTypeString = GetFullName(method.ReturnType); + 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{"); diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs index 4ba5253a..70ecad7b 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs @@ -39,35 +39,30 @@ public SourceText ToSourceText() 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("#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("\t/// 动态依赖初始化器"); + builder.AppendLine("\t[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + builder.AppendLine($"\tstatic partial class {nameof(HttpApiProxyClassInitializer)}"); + builder.AppendLine("\t{"); - builder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]"); + builder.AppendLine($"\t\t/// 初始化本程序集的动态依赖 "); + builder.AppendLine("\t\t[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($"\t\t[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("\t\tpublic static void Initialize()"); + builder.AppendLine("\t\t{"); + builder.AppendLine("\t\t}"); + builder.AppendLine("\t}"); builder.AppendLine("}"); builder.AppendLine("#pragma warning restore"); builder.AppendLine("#endif"); From 5dd519224e48941bf59a494bc048a269dd087a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 13:32:50 +0800 Subject: [PATCH 004/108] add nuget push --- nuget.push.bat | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 nuget.push.bat 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 From 6f3f8f0025d59d07ade43c8c6fe2f1db903d6d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 13:45:11 +0800 Subject: [PATCH 005/108] =?UTF-8?q?=E7=AE=80=E5=8C=96DefaultHttpApiActivat?= =?UTF-8?q?or?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/DefaultHttpApiActivator.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs index 38807522..26c83dea 100644 --- a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs @@ -14,8 +14,7 @@ public class DefaultHttpApiActivator< #endif THttpApi> : IHttpApiActivator { - private readonly Lazy> ilEmitHttpApiActivatorLazy; - private readonly SourceGeneratorHttpApiActivator? sourceGeneratorHttpApiActivator; + private readonly IHttpApiActivator httpApiActivator; /// /// 默认的THttpApi的实例创建器 @@ -26,11 +25,9 @@ public class DefaultHttpApiActivator< /// public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { - if (SourceGeneratorHttpApiActivator.IsSupported) - { - this.sourceGeneratorHttpApiActivator = new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); - } - this.ilEmitHttpApiActivatorLazy = new Lazy>(() => new ILEmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider), isThreadSafe: true); + this.httpApiActivator = SourceGeneratorHttpApiActivator.IsSupported + ? new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider) + : new ILEmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); } /// @@ -40,9 +37,7 @@ public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorP /// public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) { - return this.sourceGeneratorHttpApiActivator == null - ? this.ilEmitHttpApiActivatorLazy.Value.CreateInstance(apiInterceptor) - : this.sourceGeneratorHttpApiActivator.CreateInstance(apiInterceptor); + return this.httpApiActivator.CreateInstance(apiInterceptor); } } } \ No newline at end of file From e93b74eb40008bcfb1e392891f762dac1d1374ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 14:02:20 +0800 Subject: [PATCH 006/108] =?UTF-8?q?=E6=A3=80=E6=B5=8B=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E7=BB=A7=E6=89=BF=E5=85=B3=E7=B3=BB=E6=98=AF=E5=90=A6=E5=AD=98?= =?UTF-8?q?=E5=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/SourceGeneratorProxyClassType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs index 4fd029e9..c8ceb010 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs @@ -40,7 +40,7 @@ private static void AnalyzeAssembly(Assembly assembly) if (classType.IsClass) { var proxyClassAttr = classType.GetCustomAttribute(); - if (proxyClassAttr != null) + if (proxyClassAttr != null && proxyClassAttr.HttpApiType.IsAssignableFrom(classType)) { httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); } From a542731432672a4d0d2ca82c80140e56cb2c7c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 17:26:10 +0800 Subject: [PATCH 007/108] =?UTF-8?q?=E6=96=B9=E6=B3=95=E4=BD=BF=E7=94=A8dec?= =?UTF-8?q?laringType=E5=8F=82=E4=B8=8E=E6=AF=94=E8=BE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpApiSourceGenerator.cs | 2 +- .../SourceGenerator/HttpApiProxyClass.cs | 5 ++-- .../HttpApiProxyClassInitializer.cs | 7 ++--- .../HttpApiProxyMethodAttribute.cs | 11 ++++++-- .../SourceGeneratorHttpApiActivator.cs | 27 ++++++++++++++----- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs index 4e51ed3f..dc4a8651 100644 --- a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs +++ b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs @@ -40,7 +40,7 @@ public void Execute(GeneratorExecutionContext context) if (proxyClasses.Length > 0) { - var initializer = new HttpApiProxyClassInitializer(context.Compilation, proxyClasses); + var initializer = new HttpApiProxyClassInitializer(proxyClasses); context.AddSource(initializer.FileName, initializer.ToSourceText()); } } diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 46d9aca0..0fb158e3 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -137,9 +137,8 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, ? "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})]"); + var returnTypeString = GetFullName(method.ReturnType); + builder.AppendLine($"\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, typeof({GetFullName(interfaceType)}), \"{method.Name}\")]"); 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});"); diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs index 70ecad7b..3ba54a8f 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs @@ -10,7 +10,6 @@ namespace WebApiClientCore.Analyzers.SourceGenerator /// sealed class HttpApiProxyClassInitializer { - private readonly Compilation compilation; private readonly IEnumerable proxyClasses; /// @@ -20,12 +19,10 @@ sealed class HttpApiProxyClassInitializer /// /// HttpApi代理类初始化器 - /// - /// + /// /// - public HttpApiProxyClassInitializer(Compilation compilation, IEnumerable proxyClasses) + public HttpApiProxyClassInitializer(IEnumerable proxyClasses) { - this.compilation = compilation; this.proxyClasses = proxyClasses; } diff --git a/WebApiClientCore/HttpApiProxyMethodAttribute.cs b/WebApiClientCore/HttpApiProxyMethodAttribute.cs index 17287bfc..7945fdcc 100644 --- a/WebApiClientCore/HttpApiProxyMethodAttribute.cs +++ b/WebApiClientCore/HttpApiProxyMethodAttribute.cs @@ -15,6 +15,11 @@ public sealed class HttpApiProxyMethodAttribute : Attribute /// public int Index { get; } + /// + /// 获取方法所在的声明类型 + /// + public Type? DeclaringType { get; } + /// /// 获取名称 /// @@ -23,11 +28,13 @@ public sealed class HttpApiProxyMethodAttribute : Attribute /// /// 方法的索引特性 /// - /// 索引值,确保连续且不重复 + /// 索引值 + /// 法所在的声明类型 /// 方法的名称 - public HttpApiProxyMethodAttribute(int index, string name) + public HttpApiProxyMethodAttribute(int index, Type? declaringType, string name) { this.Index = index; + this.DeclaringType = declaringType; this.Name = name; } } diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index 1901dd70..d501d37b 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -95,12 +95,13 @@ orderby c.Index /// 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 +117,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 +138,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 +170,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()) { From 1ecb41b09f359210c65c9dce3316f04f880b0711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 6 Jun 2024 18:02:26 +0800 Subject: [PATCH 008/108] =?UTF-8?q?api=E6=95=B0=E9=87=8F=E4=B8=A5=E6=A0=BC?= =?UTF-8?q?=E6=AF=94=E8=BE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 2 +- .../SourceGeneratorHttpApiActivator.cs | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 0fb158e3..219ae374 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -89,7 +89,7 @@ public override string ToString() 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($"\tsealed class {this.ClassName} : {this.httpApiFullName}"); builder.AppendLine("\t{"); builder.AppendLine($"\t\tprivate readonly global::WebApiClientCore.IHttpApiInterceptor {this.apiInterceptorFieldName};"); diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index d501d37b..5edbfee3 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using WebApiClientCore.Exceptions; @@ -68,31 +70,35 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// 接口类型 /// 接口的实现类型 /// - private static MethodInfo[] FindApiMethods(Type httpApiType, Type proxyClassType) + private static IEnumerable FindApiMethods(Type httpApiType, Type proxyClassType) { - var apiMethods = HttpApi.FindApiMethods(httpApiType); - var classMethods = proxyClassType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); + var apiMethods = HttpApi.FindApiMethods(httpApiType) + .Select(item => new MethodFeature(item, isProxyMethod: false)) + .ToArray(); - // 按照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 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}与其代理类不匹配,请重新编译接口类型所在的项目"; 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; From b965eeff0b9b45481e1b66cf5f696708f981325f Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 6 Jun 2024 20:20:18 +0800 Subject: [PATCH 009/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/WebApiClientCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 61f2116b..f3aa7231 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -48,7 +48,7 @@ - + From 4fae272782921f3571cdc3a6cc0702bf8b5de9d2 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 6 Jun 2024 20:50:10 +0800 Subject: [PATCH 010/108] =?UTF-8?q?=E5=85=BC=E5=AE=B9v2.0.8=E5=92=8C2.0.9?= =?UTF-8?q?=E7=9A=84SourceGenerator=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 2 +- .../HttpApiProxyMethodAttribute.cs | 31 +++++++++++++++---- .../SourceGeneratorHttpApiActivator.cs | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 219ae374..5f1e1e29 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -138,7 +138,7 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, : $"new global::System.Object[] {{ {parameterNamesString} }}"; var returnTypeString = GetFullName(method.ReturnType); - builder.AppendLine($"\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, typeof({GetFullName(interfaceType)}), \"{method.Name}\")]"); + builder.AppendLine($"\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, \"{method.Name}\", typeof({GetFullName(interfaceType)}))]"); 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});"); diff --git a/WebApiClientCore/HttpApiProxyMethodAttribute.cs b/WebApiClientCore/HttpApiProxyMethodAttribute.cs index 7945fdcc..998226d6 100644 --- a/WebApiClientCore/HttpApiProxyMethodAttribute.cs +++ b/WebApiClientCore/HttpApiProxyMethodAttribute.cs @@ -13,7 +13,12 @@ public sealed class HttpApiProxyMethodAttribute : Attribute /// /// 获取索引值 /// - public int Index { get; } + public int Index { get; } = -1; + + /// + /// 获取名称 + /// + public string Name { get; } = string.Empty; /// /// 获取方法所在的声明类型 @@ -21,21 +26,35 @@ public sealed class HttpApiProxyMethodAttribute : Attribute public Type? DeclaringType { get; } /// - /// 获取名称 + /// 方法的索引特性 /// - public string Name { get; } + /// 索引值 + [Obsolete("仅为了兼容v2.0.8的SourceGenerator语法")] + public HttpApiProxyMethodAttribute(int index) + { + } /// /// 方法的索引特性 /// /// 索引值 - /// 法所在的声明类型 /// 方法的名称 - public HttpApiProxyMethodAttribute(int index, Type? declaringType, string name) + [Obsolete("仅为了兼容v2.0.9的SourceGenerator语法")] + public HttpApiProxyMethodAttribute(int index, string name) + { + } + + /// + /// 方法的索引特性 + /// + /// 索引值 + /// 方法的名称 + /// 法所在的声明类型 + public HttpApiProxyMethodAttribute(int index, string name, Type? declaringType) { this.Index = index; - this.DeclaringType = declaringType; this.Name = name; + this.DeclaringType = declaringType; } } } diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index 5edbfee3..fd923610 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -83,7 +83,7 @@ private static IEnumerable FindApiMethods(Type httpApiType, Type pro if (apiMethods.Length != classMethods.Length) { - var message = $"接口类型{httpApiType}与其代理类不匹配,请重新编译接口类型所在的项目"; + var message = $"接口类型{httpApiType}的代理类{proxyClassType}和当前版本不兼容,请将{httpApiType.Assembly.GetName().Name}项目所依赖的WebApiClientCore更新到版本v{typeof(SourceGeneratorHttpApiActivator<>).Assembly.GetName().Version}"; throw new ProxyTypeException(httpApiType, message); } From 3a424cf472fe8c55c90b92a48d3bf26d3a11b7e7 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 6 Jun 2024 21:27:16 +0800 Subject: [PATCH 011/108] .g.cs --- WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs | 2 +- .../SourceGenerator/HttpApiProxyClassInitializer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 5f1e1e29..cf9b3fd6 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -31,7 +31,7 @@ sealed class HttpApiProxyClass : IEquatable /// /// 文件名 /// - public string FileName => this.httpApi.ToDisplayString(); + public string FileName => this.httpApi.ToDisplayString() + ".g.cs"; /// /// 命名空间 diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs index 3ba54a8f..26952040 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs @@ -15,7 +15,7 @@ sealed class HttpApiProxyClassInitializer /// /// 文件名 /// - public string FileName => $"{nameof(HttpApiProxyClassInitializer)}.cs"; + public string FileName => $"{nameof(HttpApiProxyClassInitializer)}.g.cs"; /// /// HttpApi代理类初始化器 From 5c4dfba01ed8449593b410edb21ed8fb33686ef6 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 6 Jun 2024 22:33:37 +0800 Subject: [PATCH 012/108] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpApiSourceGenerator.cs | 17 ++-- .../HttpApiProxyClass.Static.cs | 39 ++++++++++ .../SourceGenerator/HttpApiProxyClass.cs | 55 ++++++------- .../HttpApiProxyClassInitializer.cs | 77 +++++++------------ .../SourceGeneratorProxyClassType.cs | 15 ++-- 5 files changed, 109 insertions(+), 94 deletions(-) create mode 100644 WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs diff --git a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs index dc4a8651..7b651261 100644 --- a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs +++ b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs @@ -26,22 +26,23 @@ public void Initialize(GeneratorInitializationContext context) 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(proxyClasses); - context.AddSource(initializer.FileName, initializer.ToSourceText()); + context.AddSource(HttpApiProxyClassStatic.FileName, HttpApiProxyClassStatic.ToSourceText()); + context.AddSource(HttpApiProxyClassInitializer.FileName, HttpApiProxyClassInitializer.ToSourceText()); + + foreach (var proxyClass in proxyClasses) + { + context.AddSource(proxyClass.FileName, proxyClass.ToSourceText()); + } } } } diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs new file mode 100644 index 00000000..450a97e4 --- /dev/null +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs @@ -0,0 +1,39 @@ +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace WebApiClientCore.Analyzers.SourceGenerator +{ + /// + /// HttpApiProxyClass的静态类 + /// + static class HttpApiProxyClassStatic + { + /// + /// 文件名 + /// + public static string FileName => $"{nameof(HttpApiProxyClass)}.g.cs"; + + /// + /// 转换为SourceText + /// + /// + public static SourceText ToSourceText() + { + var code = $$""" + #pragma warning disable + using System; + namespace WebApiClientCore + { + [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)}} + { + } + } + #pragma warning restore + """; + return SourceText.From(code, Encoding.UTF8); + } + } +} diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index cf9b3fd6..f592055b 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -31,17 +31,12 @@ sealed class HttpApiProxyClass : IEquatable /// /// 文件名 /// - public string FileName => this.httpApi.ToDisplayString() + ".g.cs"; - - /// - /// 命名空间 - /// - public string Namespace => $"WebApiClientCore.{this.httpApi.ContainingNamespace}"; + public string FileName => $"{nameof(HttpApiProxyClass)}.{this.httpApi.ToDisplayString()}.g.cs"; /// /// 类型名 /// - public string ClassName => this.httpApi.Name + "Class"; + public string ClassName => this.httpApi.Name; /// /// HttpApi代理类 @@ -68,8 +63,7 @@ private static string GetFullName(ISymbol symbol) /// /// public SourceText ToSourceText() - { - // System.Diagnostics.Debugger.Launch(); + { var code = this.ToString(); return SourceText.From(code, Encoding.UTF8); } @@ -82,24 +76,23 @@ 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($"\tsealed 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.ClassName}(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 class {this.ClassName} : {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 {this.apiInterceptorFieldName};"); + builder.AppendLine($"\t\t\tprivate readonly global::WebApiClientCore.ApiActionInvoker[] {this.actionInvokersFieldName};"); + builder.AppendLine(); + builder.AppendLine($"\t\t\tpublic {this.ClassName}(global::WebApiClientCore.IHttpApiInterceptor apiInterceptor, global::WebApiClientCore.ApiActionInvoker[] actionInvokers)"); + builder.AppendLine("\t\t\t{"); + builder.AppendLine($"\t\t\t\tthis.{this.apiInterceptorFieldName} = apiInterceptor;"); + builder.AppendLine($"\t\t\t\tthis.{this.actionInvokersFieldName} = actionInvokers;"); + builder.AppendLine("\t\t\t}"); builder.AppendLine(); var index = 0; @@ -113,7 +106,7 @@ public override string ToString() } } - + builder.AppendLine("\t\t}"); builder.AppendLine("\t}"); builder.AppendLine("}"); builder.AppendLine("#pragma warning restore"); @@ -133,16 +126,16 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, 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 parameterArrayString = string.IsNullOrEmpty(parameterNamesString) ? "global::System.Array.Empty()" : $"new global::System.Object[] {{ {parameterNamesString} }}"; var returnTypeString = GetFullName(method.ReturnType); - builder.AppendLine($"\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, \"{method.Name}\", typeof({GetFullName(interfaceType)}))]"); - 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}"); + builder.AppendLine($"\t\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, \"{method.Name}\", typeof({GetFullName(interfaceType)}))]"); + builder.AppendLine($"\t\t\t{returnTypeString} {GetFullName(interfaceType)}.{method.Name}({parametersString})"); + builder.AppendLine("\t\t\t{"); + builder.AppendLine($"\t\t\t\treturn ({returnTypeString})this.{this.apiInterceptorFieldName}.Intercept(this.{this.actionInvokersFieldName}[{index}], {parameterArrayString});"); + builder.AppendLine("\t\t\t}"); return builder.ToString(); } diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs index 26952040..2d060d8f 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs @@ -1,70 +1,47 @@ -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 IEnumerable proxyClasses; - /// /// 文件名 /// - public string FileName => $"{nameof(HttpApiProxyClassInitializer)}.g.cs"; - - /// - /// HttpApi代理类初始化器 - /// - /// - public HttpApiProxyClassInitializer(IEnumerable proxyClasses) - { - this.proxyClasses = proxyClasses; - } + public static string FileName => $"{nameof(HttpApiProxyClassInitializer)}.g.cs"; /// /// 转换为SourceText /// /// - public SourceText ToSourceText() + public static SourceText ToSourceText() { - var code = this.ToString(); + var code = $$""" + #if NET5_0_OR_GREATER + #pragma warning disable + namespace WebApiClientCore + { + /// 动态依赖初始化器 + [global::System.Reflection.Obfuscation(Exclude = true)] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static partial class HttpApiProxyClassInitializer + { + /// 初始化本程序集的动态依赖 + [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() + { + } + } + } + #pragma warning restore + #endif + """; 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("\t/// 动态依赖初始化器"); - builder.AppendLine("\t[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); - builder.AppendLine($"\tstatic partial class {nameof(HttpApiProxyClassInitializer)}"); - builder.AppendLine("\t{"); - - builder.AppendLine($"\t\t/// 初始化本程序集的动态依赖 "); - builder.AppendLine("\t\t[global::System.Runtime.CompilerServices.ModuleInitializer]"); - foreach (var item in this.proxyClasses) - { - builder.AppendLine($"\t\t[global::System.Diagnostics.CodeAnalysis.DynamicDependency(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All, typeof(global::{item.Namespace}.{item.ClassName}))]"); - } - - builder.AppendLine("\t\tpublic static void Initialize()"); - builder.AppendLine("\t\t{"); - builder.AppendLine("\t\t}"); - builder.AppendLine("\t}"); - builder.AppendLine("}"); - builder.AppendLine("#pragma warning restore"); - builder.AppendLine("#endif"); - return builder.ToString(); - } - } } diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs index c8ceb010..5ef981b0 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs @@ -13,6 +13,7 @@ static class SourceGeneratorProxyClassType private static readonly object syncRoot = new(); private static readonly HashSet assemblies = []; private static readonly ConcurrentDictionary httpApiProxyClassTable = []; + private const string HttpApiProxyClassTypeName = "WebApiClientCore.HttpApiProxyClass"; /// /// 查找指定接口类型的代理类类型 @@ -35,14 +36,18 @@ private static void AnalyzeAssembly(Assembly assembly) { if (AddAssembly(assembly)) { - foreach (var classType in assembly.GetTypes()) + var httpApiProxyClass = assembly.GetType(HttpApiProxyClassTypeName); + if (httpApiProxyClass != null) { - if (classType.IsClass) + foreach (var classType in httpApiProxyClass.GetNestedTypes(BindingFlags.NonPublic)) { - var proxyClassAttr = classType.GetCustomAttribute(); - if (proxyClassAttr != null && proxyClassAttr.HttpApiType.IsAssignableFrom(classType)) + if (classType.IsClass) { - httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); + var proxyClassAttr = classType.GetCustomAttribute(); + if (proxyClassAttr != null && proxyClassAttr.HttpApiType.IsAssignableFrom(classType)) + { + httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); + } } } } From 64e2696a8fc1baa2abf4af40e3273fed5394ec40 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 6 Jun 2024 22:59:37 +0800 Subject: [PATCH 013/108] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E5=8A=A8=E6=80=81=E4=BE=9D=E8=B5=96=E6=8C=87=E5=90=91?= =?UTF-8?q?=E8=87=AA=E8=BA=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpApiSourceGenerator.cs | 4 +- .../HttpApiProxyClass.Static.cs | 39 ------------------- .../HttpApiProxyClassInitializer.cs | 21 +++++----- 3 files changed, 12 insertions(+), 52 deletions(-) delete mode 100644 WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs diff --git a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs index 7b651261..944a4573 100644 --- a/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs +++ b/WebApiClientCore.Analyzers/HttpApiSourceGenerator.cs @@ -26,7 +26,7 @@ public void Initialize(GeneratorInitializationContext context) public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is HttpApiSyntaxReceiver receiver) - { + { // System.Diagnostics.Debugger.Launch(); var proxyClasses = receiver .GetHttpApiTypes(context.Compilation) @@ -36,9 +36,7 @@ public void Execute(GeneratorExecutionContext context) if (proxyClasses.Length > 0) { - context.AddSource(HttpApiProxyClassStatic.FileName, HttpApiProxyClassStatic.ToSourceText()); context.AddSource(HttpApiProxyClassInitializer.FileName, HttpApiProxyClassInitializer.ToSourceText()); - foreach (var proxyClass in proxyClasses) { context.AddSource(proxyClass.FileName, proxyClass.ToSourceText()); diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs deleted file mode 100644 index 450a97e4..00000000 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.Static.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.CodeAnalysis.Text; -using System.Text; - -namespace WebApiClientCore.Analyzers.SourceGenerator -{ - /// - /// HttpApiProxyClass的静态类 - /// - static class HttpApiProxyClassStatic - { - /// - /// 文件名 - /// - public static string FileName => $"{nameof(HttpApiProxyClass)}.g.cs"; - - /// - /// 转换为SourceText - /// - /// - public static SourceText ToSourceText() - { - var code = $$""" - #pragma warning disable - using System; - namespace WebApiClientCore - { - [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)}} - { - } - } - #pragma warning restore - """; - return SourceText.From(code, Encoding.UTF8); - } - } -} diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs index 2d060d8f..dfef221f 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClassInitializer.cs @@ -4,14 +4,14 @@ namespace WebApiClientCore.Analyzers.SourceGenerator { /// - /// HttpApiProxyClass初始化器 + /// HttpApiProxyClass的静态类初始器 /// static class HttpApiProxyClassInitializer { /// /// 文件名 /// - public static string FileName => $"{nameof(HttpApiProxyClassInitializer)}.g.cs"; + public static string FileName => $"{nameof(HttpApiProxyClass)}.g.cs"; /// /// 转换为SourceText @@ -20,26 +20,27 @@ static class HttpApiProxyClassInitializer public static SourceText ToSourceText() { var code = $$""" - #if NET5_0_OR_GREATER #pragma warning disable + using System; namespace WebApiClientCore { - /// 动态依赖初始化器 - [global::System.Reflection.Obfuscation(Exclude = true)] + /// HttpApi代理类 + [global::System.Reflection.Obfuscation(Exclude = true)] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static partial class HttpApiProxyClassInitializer - { - /// 初始化本程序集的动态依赖 + 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 - #endif """; return SourceText.From(code, Encoding.UTF8); } From c98be11c3f82f553abc429c7d26b36ad54ba700b Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 6 Jun 2024 23:41:42 +0800 Subject: [PATCH 014/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8sg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Benchmarks/Program.cs | 2 +- WebApiClientCore.Benchmarks/Requests/Benchmark.cs | 14 +------------- .../Requests/GetBenchmark.cs | 8 ++++---- WebApiClientCore.Benchmarks/Requests/IRefitApi.cs | 2 +- .../Requests/IWebApiClientCoreApi.cs | 2 +- .../Requests/PostJsonBenchmark.cs | 8 ++++---- .../Requests/PutFormBenchmark.cs | 8 ++++---- .../WebApiClientCore.Benchmarks.csproj | 1 + 8 files changed, 17 insertions(+), 28 deletions(-) diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index e2cac205..dddd8cf6 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -9,7 +9,7 @@ class Program static void Main(string[] args) { var benchmarkTypes = typeof(Program).Assembly.GetTypes() - .Where(item => typeof(IBenchmark).IsAssignableFrom(item)) + .Where(item => typeof(Requests.Benchmark).IsAssignableFrom(item)) .Where(item => item.IsAbstract == false && item.IsClass); foreach (var item in benchmarkTypes) diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 6d1a0283..8df2d182 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -3,7 +3,6 @@ using Refit; using System; using System.Net.Http; -using System.Threading.Tasks; namespace WebApiClientCore.Benchmarks.Requests { @@ -15,7 +14,7 @@ public abstract class Benchmark : IBenchmark [GlobalSetup] - public async Task SetupAsync() + public void Setup() { var services = new ServiceCollection(); @@ -41,17 +40,6 @@ public async Task SetupAsync() .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(); - - await core.GetAsyc("id"); - await core.PostJsonAsync(new Model { }); - - await refit.GetAsyc("id"); - await refit.PostJsonAsync(new Model { }); } } } diff --git a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs index f44ffab3..2e9be924 100644 --- a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs @@ -38,8 +38,8 @@ public async Task HttpClient_GetAsync() public async Task WebApiClientCore_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await banchmarkApi.GetAsyc(id: "id"); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.GetAsync(id: "id"); } @@ -51,8 +51,8 @@ public async Task WebApiClientCore_GetAsync() public async Task Refit_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await banchmarkApi.GetAsyc(id: "id"); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.GetAsync(id: "id"); } } } diff --git a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs index 868da657..fd5291db 100644 --- a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs @@ -6,7 +6,7 @@ namespace WebApiClientCore.Benchmarks.Requests public interface IRefitApi { [Get("/benchmarks/{id}")] - Task GetAsyc(string id); + Task GetAsync(string id); [Post("/benchmarks")] Task PostJsonAsync([Body(BodySerializationMethod.Serialized)]Model model); diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs index fd118877..9346b240 100644 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs @@ -6,7 +6,7 @@ namespace WebApiClientCore.Benchmarks.Requests public interface IWebApiClientCoreApi { [HttpGet("/benchmarks/{id}")] - Task GetAsyc(string id); + Task GetAsync(string id); [HttpPost("/benchmarks")] Task PostJsonAsync([JsonContent] Model model); diff --git a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs index bec2b14c..167e83c2 100644 --- a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs @@ -42,9 +42,9 @@ public async Task HttpClient_PostJsonAsync() public async Task WebApiClientCore_PostJsonAsync() { using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); var input = new Model { A = "a" }; - return await banchmarkApi.PostJsonAsync(input); + return await benchmarkApi.PostJsonAsync(input); } @@ -52,9 +52,9 @@ public async Task WebApiClientCore_PostJsonAsync() public async Task Refit_PostJsonAsync() { using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); var input = new Model { A = "a" }; - return await banchmarkApi.PostJsonAsync(input); + return await benchmarkApi.PostJsonAsync(input); } } } diff --git a/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs index 67bd3c19..ec8c7c63 100644 --- a/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs @@ -18,9 +18,9 @@ public class PutFormBenchmark : Benchmark public async Task WebApiClientCore_PutFormAsync() { using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); var input = new Model { A = "a" }; - return await banchmarkApi.PutFormAsync("id001", input); + return await benchmarkApi.PutFormAsync("id001", input); } @@ -28,9 +28,9 @@ public async Task WebApiClientCore_PutFormAsync() public async Task Refit_PutFormAsync() { using var scope = this.ServiceProvider.CreateScope(); - var banchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); var input = new Model { A = "a" }; - return await banchmarkApi.PutFormAsync("id001", input); + return await benchmarkApi.PutFormAsync("id001", input); } } } diff --git a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj index dbbb25f8..fbfd41ac 100644 --- a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj +++ b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj @@ -16,6 +16,7 @@ + From 896b26be60f8168ad64b8fbdc36990cf25207cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 7 Jun 2024 08:43:24 +0800 Subject: [PATCH 015/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B6=E8=A1=8C?= =?UTF-8?q?=E6=9F=A5=E6=89=BE=E4=BB=A3=E7=90=86=E7=B1=BB=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGeneratorProxyClassType.cs | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs index 5ef981b0..44768dee 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; @@ -12,7 +11,7 @@ static class SourceGeneratorProxyClassType { private static readonly object syncRoot = new(); private static readonly HashSet assemblies = []; - private static readonly ConcurrentDictionary httpApiProxyClassTable = []; + private static readonly Dictionary httpApiProxyClassTable = []; private const string HttpApiProxyClassTypeName = "WebApiClientCore.HttpApiProxyClass"; /// @@ -22,7 +21,13 @@ static class SourceGeneratorProxyClassType /// public static Type? Find(Type httpApiType) { - AnalyzeAssembly(httpApiType.Assembly); + lock (syncRoot) + { + if (assemblies.Add(httpApiType.Assembly)) + { + AnalyzeAssembly(httpApiType.Assembly); + } + } if (httpApiProxyClassTable.TryGetValue(httpApiType, out var proxyClassType)) { @@ -34,32 +39,18 @@ static class SourceGeneratorProxyClassType private static void AnalyzeAssembly(Assembly assembly) { - if (AddAssembly(assembly)) + var httpApiProxyClass = assembly.GetType(HttpApiProxyClassTypeName); + if (httpApiProxyClass != null) { - var httpApiProxyClass = assembly.GetType(HttpApiProxyClassTypeName); - if (httpApiProxyClass != null) + foreach (var classType in httpApiProxyClass.GetNestedTypes(BindingFlags.NonPublic)) { - foreach (var classType in httpApiProxyClass.GetNestedTypes(BindingFlags.NonPublic)) + var proxyClassAttr = classType.GetCustomAttribute(); + if (proxyClassAttr != null && proxyClassAttr.HttpApiType.IsAssignableFrom(classType)) { - if (classType.IsClass) - { - var proxyClassAttr = classType.GetCustomAttribute(); - if (proxyClassAttr != null && proxyClassAttr.HttpApiType.IsAssignableFrom(classType)) - { - httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); - } - } + httpApiProxyClassTable.TryAdd(proxyClassAttr.HttpApiType, classType); } } } } - - private static bool AddAssembly(Assembly assembly) - { - lock (syncRoot) - { - return assemblies.Add(assembly); - } - } } } From 7799cf50c0a6d9db4567898cfb04015988de05df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 7 Jun 2024 09:26:15 +0800 Subject: [PATCH 016/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0benchmark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Benchmarks/Requests/Benchmark.cs | 3 ++- ...Benchmarks.Requests.GetBenchmark-report-github.md | 10 +++++----- ...marks.Requests.PostJsonBenchmark-report-github.md | 10 +++++----- ...hmarks.Requests.PutFormBenchmark-report-github.md | 12 ++++++------ .../SourceGeneratorHttpApiActivator.cs | 7 +++++-- ...assType.cs => SourceGeneratorProxyClassFinder.cs} | 2 +- 6 files changed, 24 insertions(+), 20 deletions(-) rename WebApiClientCore/Implementations/{SourceGeneratorProxyClassType.cs => SourceGeneratorProxyClassFinder.cs} (97%) diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 8df2d182..775769d1 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -34,12 +34,13 @@ public void Setup() services .AddRefitClient(new RefitSettings { - Buffered = true, }) .AddHttpMessageHandler(() => new MockResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); this.ServiceProvider = services.BuildServiceProvider(); + this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); } } } diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md index 21b1f507..d7ff935d 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md @@ -1,7 +1,7 @@ ``` -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 +BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3) +Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores [Host] : .NET 8.0.4, X64 NativeAOT AVX2 Job=InProcess Toolchain=InProcessEmitToolchain @@ -9,6 +9,6 @@ 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 | +| HttpClient_GetAsync | 3.059 μs | 0.1315 μs | 0.3816 μs | 0.49 | 0.09 | 0.4959 | 2.03 KB | 0.51 | +| WebApiClientCore_GetAsync | 6.277 μs | 0.2695 μs | 0.7903 μs | 1.00 | 0.00 | 0.9766 | 4.02 KB | 1.00 | +| Refit_GetAsync | 14.295 μs | 0.4401 μs | 1.2626 μs | 2.30 | 0.31 | 1.2817 | 5.34 KB | 1.33 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md index fee92064..668c5b3f 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md @@ -1,7 +1,7 @@ ``` -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 +BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3) +Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores [Host] : .NET 8.0.4, X64 NativeAOT AVX2 Job=InProcess Toolchain=InProcessEmitToolchain @@ -9,6 +9,6 @@ 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 | +| HttpClient_PostJsonAsync | 3.804 μs | 0.1678 μs | 0.4813 μs | 0.60 | 0.11 | 0.5646 | 2.31 KB | 0.55 | +| WebApiClientCore_PostJsonAsync | 6.528 μs | 0.2695 μs | 0.7945 μs | 1.00 | 0.00 | 1.0147 | 4.17 KB | 1.00 | +| Refit_PostJsonAsync | 13.823 μs | 0.5368 μs | 1.5658 μs | 2.16 | 0.38 | 1.4038 | 5.83 KB | 1.40 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md index 1082bc83..b9f4ce95 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md @@ -1,13 +1,13 @@ ``` -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 +BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3) +Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 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 | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------ |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_PutFormAsync | 10.45 μs | 0.415 μs | 1.204 μs | 1.00 | 0.00 | 1.2207 | 5 KB | 1.00 | +| Refit_PutFormAsync | 22.70 μs | 1.261 μs | 3.717 μs | 2.19 | 0.33 | 1.6785 | 6.89 KB | 1.38 | diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index fd923610..fe75bf2d 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -21,7 +22,7 @@ public class SourceGeneratorHttpApiActivator< { 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)); /// /// 获取是否支持 @@ -51,7 +52,9 @@ public SourceGeneratorHttpApiActivator(IApiActionDescriptorProvider apiActionDes .Select(actionInvokerProvider.CreateActionInvoker) .ToArray(); - this.activator = LambdaUtil.CreateCtorFunc(proxyClassType); + this.activator = RuntimeFeature.IsDynamicCodeSupported + ? LambdaUtil.CreateCtorFunc(proxyClassType) + : (interceptor, invokers) => proxyClassType.CreateInstance(interceptor, invokers); } /// diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs similarity index 97% rename from WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs rename to WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs index 44768dee..f14fb4e4 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassType.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Implementations /// /// 提供获取SourceGenerator生成的代理类型 /// - static class SourceGeneratorProxyClassType + static class SourceGeneratorProxyClassFinder { private static readonly object syncRoot = new(); private static readonly HashSet assemblies = []; From 12896676457850dd97406a9348b30b391b417f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 7 Jun 2024 09:34:02 +0800 Subject: [PATCH 017/108] sealed --- WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs | 2 +- .../Implementations/SourceGeneratorHttpApiActivator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs index 43108b67..9d8e8119 100644 --- a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs @@ -11,7 +11,7 @@ namespace WebApiClientCore.Implementations /// 运行时使用ILEmit动态创建THttpApi的代理类和代理类实例 /// /// - public class ILEmitHttpApiActivator< + public sealed class ILEmitHttpApiActivator< #if NET5_0_OR_GREATER [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] #endif diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index fe75bf2d..a3c6adb9 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -14,7 +14,7 @@ namespace WebApiClientCore.Implementations /// 通过查找类型代理类型创建实例 /// /// - public class SourceGeneratorHttpApiActivator< + public sealed class SourceGeneratorHttpApiActivator< #if NET5_0_OR_GREATER [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] #endif From 442066b829d45294a1c06e6b1f61d8533f08ea37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 7 Jun 2024 11:31:14 +0800 Subject: [PATCH 018/108] Update README --- README.md | 40 +++++++++++++++++++++------------------- README_zh.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 README_zh.md diff --git a/README.md b/README.md index b81af1a1..46fa5988 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. -### 文档支持 +### Documentation support https://webapiclient.github.io/ 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/ From aa0250557678dccb180a9cc203615e887195cb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 7 Jun 2024 16:26:52 +0800 Subject: [PATCH 019/108] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=90=8D=E7=9B=B8=E5=90=8C=E6=97=B6=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index f592055b..5a69d3eb 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -31,12 +31,12 @@ sealed class HttpApiProxyClass : IEquatable /// /// 文件名 /// - public string FileName => $"{nameof(HttpApiProxyClass)}.{this.httpApi.ToDisplayString()}.g.cs"; + public string FileName { get; } /// /// 类型名 /// - public string ClassName => this.httpApi.Name; + public string ClassName { get; } /// /// HttpApi代理类 @@ -46,6 +46,10 @@ public HttpApiProxyClass(INamedTypeSymbol httpApi) { this.httpApi = httpApi; this.httpApiFullName = GetFullName(httpApi); + + var httpApiName = httpApi.ToDisplayString(); + this.FileName = $"{nameof(HttpApiProxyClass)}.{httpApiName}.g.cs"; + this.ClassName = httpApiName.Replace(".", "_"); } /// @@ -63,7 +67,7 @@ private static string GetFullName(ISymbol symbol) /// /// public SourceText ToSourceText() - { + { var code = this.ToString(); return SourceText.From(code, Encoding.UTF8); } From 0e6e4e0fc5d2be1da876ddd9d03573702a9ce1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 7 Jun 2024 16:41:11 +0800 Subject: [PATCH 020/108] =?UTF-8?q?=E5=8E=BB=E6=8E=89ClassName=E5=B1=9E?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 5a69d3eb..0205cdbd 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -12,32 +12,18 @@ namespace WebApiClientCore.Analyzers.SourceGenerator /// sealed class HttpApiProxyClass : IEquatable { - /// - /// 接口符号 - /// private readonly INamedTypeSymbol httpApi; private readonly string httpApiFullName; + private readonly string proxyClassName; - /// - /// 拦截器变量名 - /// - private readonly string apiInterceptorFieldName = "_apiInterceptor"; - - /// - /// action执行器变量名 - /// - private readonly string actionInvokersFieldName = "_actionInvokers"; + private const string ApiInterceptorFieldName = "_apiInterceptor"; + private const string ActionInvokersFieldName = "_actionInvokers"; /// /// 文件名 /// public string FileName { get; } - /// - /// 类型名 - /// - public string ClassName { get; } - /// /// HttpApi代理类 /// @@ -46,10 +32,8 @@ public HttpApiProxyClass(INamedTypeSymbol httpApi) { this.httpApi = httpApi; this.httpApiFullName = GetFullName(httpApi); - - var httpApiName = httpApi.ToDisplayString(); - this.FileName = $"{nameof(HttpApiProxyClass)}.{httpApiName}.g.cs"; - this.ClassName = httpApiName.Replace(".", "_"); + this.proxyClassName = httpApi.ToDisplayString().Replace(".", "_"); + this.FileName = $"{nameof(HttpApiProxyClass)}.{proxyClassName}.g.cs"; } /// @@ -86,16 +70,16 @@ public override string ToString() builder.AppendLine("\t{"); 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 class {this.ClassName} : {this.httpApiFullName}"); + builder.AppendLine($"\t\tsealed class {this.proxyClassName} : {this.httpApiFullName}"); builder.AppendLine("\t\t{"); - builder.AppendLine($"\t\t\tprivate readonly global::WebApiClientCore.IHttpApiInterceptor {this.apiInterceptorFieldName};"); - builder.AppendLine($"\t\t\tprivate readonly global::WebApiClientCore.ApiActionInvoker[] {this.actionInvokersFieldName};"); + 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.ClassName}(global::WebApiClientCore.IHttpApiInterceptor apiInterceptor, global::WebApiClientCore.ApiActionInvoker[] actionInvokers)"); + 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.{this.apiInterceptorFieldName} = apiInterceptor;"); - builder.AppendLine($"\t\t\t\tthis.{this.actionInvokersFieldName} = actionInvokers;"); + builder.AppendLine($"\t\t\t\tthis.{ApiInterceptorFieldName} = apiInterceptor;"); + builder.AppendLine($"\t\t\t\tthis.{ActionInvokersFieldName} = actionInvokers;"); builder.AppendLine("\t\t\t}"); builder.AppendLine(); @@ -138,7 +122,7 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, builder.AppendLine($"\t\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, \"{method.Name}\", typeof({GetFullName(interfaceType)}))]"); builder.AppendLine($"\t\t\t{returnTypeString} {GetFullName(interfaceType)}.{method.Name}({parametersString})"); builder.AppendLine("\t\t\t{"); - builder.AppendLine($"\t\t\t\treturn ({returnTypeString})this.{this.apiInterceptorFieldName}.Intercept(this.{this.actionInvokersFieldName}[{index}], {parameterArrayString});"); + builder.AppendLine($"\t\t\t\treturn ({returnTypeString})this.{ApiInterceptorFieldName}.Intercept(this.{ActionInvokersFieldName}[{index}], {parameterArrayString});"); builder.AppendLine("\t\t\t}"); return builder.ToString(); } From e42e4496a45991e4816aa0c02f6ce6b3d48a740a Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 19:45:01 +0800 Subject: [PATCH 021/108] =?UTF-8?q?=E4=BF=AE=E6=94=B9nuget=E6=89=93?= =?UTF-8?q?=E5=8C=85=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- .../WebApiClientCore.Abstractions.csproj | 2 +- .../WebApiClientCore.Analyzers.csproj | 2 +- ...WebApiClientCore.Extensions.JsonRpc.csproj | 2 +- ...lientCore.Extensions.NewtonsoftJson.csproj | 2 +- .../WebApiClientCore.Extensions.OAuths.csproj | 2 +- .../NugetPackage/tools/install.ps1 | 49 ---------------- .../NugetPackage/tools/uninstall.ps1 | 56 ------------------- WebApiClientCore/WebApiClientCore.csproj | 16 ++---- 9 files changed, 10 insertions(+), 123 deletions(-) delete mode 100644 WebApiClientCore/NugetPackage/tools/install.ps1 delete mode 100644 WebApiClientCore/NugetPackage/tools/uninstall.ps1 diff --git a/Directory.Build.props b/Directory.Build.props index 1f3825f9..b2ecb7a3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.1.0 + 2.1.1 Copyright © laojiu 2017-2024 IDE0057;IDE0290 diff --git a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj index 4e50e9fc..692d8732 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -6,7 +6,7 @@ WebApiClientCore WebApiClientCore.Abstractions - $(TargetPath)\$(AssemblyName).xml + $(OutputPath)\$(AssemblyName).xml WebApiClientCore的接口与抽象类型 WebApiClientCore的接口与抽象类型 diff --git a/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj b/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj index 254ad3d2..4bd7c2bf 100644 --- a/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj +++ b/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj @@ -3,7 +3,7 @@ enable netstandard2.0 - $(TargetPath)\$(AssemblyName).xml + $(OutputPath)\$(AssemblyName).xml false false true diff --git a/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj b/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj index 83cfbd95..672fb9b2 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 + $(OutputPath)\$(AssemblyName).xml true Sign.snk diff --git a/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj b/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj index 5577b232..59824ea2 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 + $(OutputPath)\$(AssemblyName).xml true Sign.snk diff --git a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj index def5a803..34c046b7 100644 --- a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj +++ b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj @@ -3,7 +3,7 @@ enable netstandard2.1;net5.0 - $(TargetPath)\$(AssemblyName).xml + $(OutputPath)\$(AssemblyName).xml true Sign.snk 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/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index f3aa7231..f0d60084 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -3,7 +3,7 @@ enable netstandard2.1;net5.0 - $(TargetPath)\$(AssemblyName).xml + $(OutputPath)\$(AssemblyName).xml .NetCore声明式的Http客户端库 一款基于HttpClient封装,只需要定义c#接口并修饰相关特性,即可异步调用远程http接口的客户端库 @@ -19,14 +19,14 @@ - + - - + + @@ -43,12 +43,4 @@ - - - - - - - - From a6835e3181b5dd2ee1f7221040404306ef146be1 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 19:48:27 +0800 Subject: [PATCH 022/108] pack --- WebApiClientCore/WebApiClientCore.csproj | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index f0d60084..5dc5e3a5 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -19,11 +19,6 @@ - - - - - @@ -43,4 +38,8 @@ + + + + From 1cf39632b1e7ad3b438e4ec116c529845acc058e Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 20:01:49 +0800 Subject: [PATCH 023/108] =?UTF-8?q?HttpApiProxyClass=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 0205cdbd..79b2d8f1 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -8,7 +8,7 @@ namespace WebApiClientCore.Analyzers.SourceGenerator { /// - /// HttpApi代理类 + /// HttpApi代理类生成器 /// sealed class HttpApiProxyClass : IEquatable { @@ -25,7 +25,7 @@ sealed class HttpApiProxyClass : IEquatable public string FileName { get; } /// - /// HttpApi代理类 + /// HttpApi代理类生成器 /// /// public HttpApiProxyClass(INamedTypeSymbol httpApi) @@ -70,7 +70,7 @@ public override string ToString() builder.AppendLine("\t{"); 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 class {this.proxyClassName} : {this.httpApiFullName}"); + builder.AppendLine($"\t\tsealed partial class {this.proxyClassName} : {this.httpApiFullName}"); builder.AppendLine("\t\t{"); builder.AppendLine($"\t\t\tprivate readonly global::WebApiClientCore.IHttpApiInterceptor {ApiInterceptorFieldName};"); @@ -84,11 +84,11 @@ public override string ToString() 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; } @@ -105,11 +105,11 @@ 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}")); @@ -119,8 +119,10 @@ private string BuildMethod(INamedTypeSymbol interfaceType, IMethodSymbol method, : $"new global::System.Object[] {{ {parameterNamesString} }}"; var returnTypeString = GetFullName(method.ReturnType); - builder.AppendLine($"\t\t\t[global::WebApiClientCore.HttpApiProxyMethod({index}, \"{method.Name}\", typeof({GetFullName(interfaceType)}))]"); - builder.AppendLine($"\t\t\t{returnTypeString} {GetFullName(interfaceType)}.{method.Name}({parametersString})"); + 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}"); @@ -132,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; } /// @@ -142,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); } /// From 33a0fbab117b8b0de80c4ceee8c1759193a5018d Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 20:35:14 +0800 Subject: [PATCH 024/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0PrependJsonSerializer?= =?UTF-8?q?Context=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppAot/Program.cs | 5 +---- WebApiClientCore.Abstractions/HttpApiOptions.cs | 13 +++++++++++++ .../WebApiClientCore.Abstractions.csproj | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) 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/WebApiClientCore.Abstractions/HttpApiOptions.cs b/WebApiClientCore.Abstractions/HttpApiOptions.cs index 708035b0..196e3c8f 100644 --- a/WebApiClientCore.Abstractions/HttpApiOptions.cs +++ b/WebApiClientCore.Abstractions/HttpApiOptions.cs @@ -103,5 +103,18 @@ private static JsonSerializerOptions CreateJsonDeserializeOptions() 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/WebApiClientCore.Abstractions.csproj b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj index 692d8732..abca34f8 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -2,7 +2,7 @@ enable - netstandard2.1;net5.0 + netstandard2.1;net5.0;net8.0 WebApiClientCore WebApiClientCore.Abstractions From 098f8e6c55c8e5e2597521f0439d4b6de01a78c0 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 21:03:57 +0800 Subject: [PATCH 025/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E6=96=B9=E6=A1=88=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.sln | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 4b0e40a5bce69709a01b05dc9cf0f969520da2f7 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 22:51:43 +0800 Subject: [PATCH 026/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8DHttpApi.GetName()?= =?UTF-8?q?=E4=BD=BF=E7=94=A8.=E5=88=86=E5=89=B2=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E7=9A=84=E7=8E=B0=E8=B1=A1=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/HttpApi.cs | 3 ++- WebApiClientCore/Internals/ValueStringBuilder.cs | 11 ++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/WebApiClientCore/HttpApi.cs b/WebApiClientCore/HttpApi.cs index a680e252..577f4779 100644 --- a/WebApiClientCore/HttpApi.cs +++ b/WebApiClientCore/HttpApi.cs @@ -38,7 +38,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); diff --git a/WebApiClientCore/Internals/ValueStringBuilder.cs b/WebApiClientCore/Internals/ValueStringBuilder.cs index ff6ac301..bf112773 100644 --- a/WebApiClientCore/Internals/ValueStringBuilder.cs +++ b/WebApiClientCore/Internals/ValueStringBuilder.cs @@ -26,9 +26,8 @@ public ValueStringBuilder(Span buffer) /// /// 添加char /// - /// - /// - public ValueStringBuilder Append(char value) + /// + public void Append(char value) { var newSize = this.index + 1; if (newSize > this.chars.Length) @@ -38,7 +37,6 @@ public ValueStringBuilder Append(char value) this.chars[this.index..][0] = value; this.index = newSize; - return this; } /// @@ -46,11 +44,11 @@ public ValueStringBuilder Append(char value) /// /// /// - 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,6 @@ public ValueStringBuilder Append(ReadOnlySpan value) value.CopyTo(this.chars[this.index..]); this.index = newSize; - return this; } /// From 3016b9f51f9a8fb5b8611b7bcc1a4c577798ffc8 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 22:56:14 +0800 Subject: [PATCH 027/108] add test --- WebApiClientCore.Test/HttpApiTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) 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() { From 56aa557e7067bcfeb73aedf7f205c1d1c59a605b Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 23:22:04 +0800 Subject: [PATCH 028/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8ApiReturnNotSupported?= =?UTF-8?q?Exception=E8=A6=86=E7=9B=96=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84ApiReturnNotSupportedExteption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiReturnNotSupportedException.cs | 17 +++++++++++++++++ .../ApiReturnNotSupportedExteption.cs | 2 ++ .../Implementations/DefaultApiActionInvoker.cs | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 WebApiClientCore/Exceptions/ApiReturnNotSupportedException.cs 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/Implementations/DefaultApiActionInvoker.cs b/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs index ac3ff6a4..febce382 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs @@ -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 } From e56c0fb2791802f087b4a11171029f80642583ca Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 7 Jun 2024 23:34:05 +0800 Subject: [PATCH 029/108] =?UTF-8?q?null=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.OpenApi.SourceGenerator/CSharpHtml.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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(' '); } } } From 013af1816414b87089f4178067037f6642c0701a Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sat, 8 Jun 2024 23:03:46 +0800 Subject: [PATCH 030/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Attributes/FilterAttributes/LoggingFilterAttribute.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index 4cd4bbd3..b1c7b7d5 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -151,13 +151,14 @@ protected virtual Task WriteLogAsync(ApiResponseContext context, LogMessage logM /// 日志消息 protected virtual void WriteLog(ILogger logger, LogMessage logMessage) { + // .NET6之后可以适配LoggerMessage以实现高性能日志 if (logMessage.Exception == null) { - logger.LogInformation(logMessage.ToString()); + logger.LogInformation("{logMessage}", logMessage); } else { - logger.LogError(logMessage.ToString()); + logger.LogError("{logMessage}", logMessage); } } } From 80bf60e5b9f0e33e8704418a36ede669f97afb42 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sat, 8 Jun 2024 23:06:35 +0800 Subject: [PATCH 031/108] =?UTF-8?q?=E5=8E=BB=E6=8E=89await?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Attributes/ActionAttributes/FormFieldAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs b/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs index 6f10edd2..58021f7e 100644 --- a/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ActionAttributes/FormFieldAttribute.cs @@ -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); } } } From 4aca88fbc360232e3c9079c20629e2d2bb2bfd84 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Tue, 11 Jun 2024 07:04:36 +0800 Subject: [PATCH 032/108] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=90=8D=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/HttpContents/JsonPatchContent.cs | 6 +++--- WebApiClientCore/Parameters/JsonPatchDocument.cs | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/WebApiClientCore/HttpContents/JsonPatchContent.cs b/WebApiClientCore/HttpContents/JsonPatchContent.cs index 5b787bff..132a4b11 100644 --- a/WebApiClientCore/HttpContents/JsonPatchContent.cs +++ b/WebApiClientCore/HttpContents/JsonPatchContent.cs @@ -25,12 +25,12 @@ public JsonPatchContent() /// /// utf8的JsonPatch内容 /// - /// patch操作项 + /// patch操作项 /// json序列化选项 - public JsonPatchContent(IEnumerable oprations, JsonSerializerOptions? jsonSerializerOptions) + public JsonPatchContent(IEnumerable operations, JsonSerializerOptions? jsonSerializerOptions) : base(MediaType) { - JsonBufferSerializer.Serialize(this, oprations, jsonSerializerOptions); + JsonBufferSerializer.Serialize(this, operations, jsonSerializerOptions); } } } diff --git a/WebApiClientCore/Parameters/JsonPatchDocument.cs b/WebApiClientCore/Parameters/JsonPatchDocument.cs index 87e0461e..267f214a 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -17,7 +17,7 @@ public class JsonPatchDocument : IApiParameter /// /// 操作列表 /// - private readonly List oprations = new(); + private readonly List operations = new(); /// /// Add操作 @@ -31,7 +31,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 +45,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 +60,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 }); } /// @@ -76,7 +76,7 @@ public Task OnRequestAsync(ApiParameterContext context) } var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = new JsonPatchContent(this.oprations, options); + context.HttpContext.RequestMessage.Content = new JsonPatchContent(this.operations, options); return Task.CompletedTask; } @@ -95,7 +95,7 @@ private class DebugView /// 查看的内容 /// [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public List Oprations => this.target.oprations; + public List Oprations => this.target.operations; /// /// 调试视图 From 1db976bb711d5925feb1f7fd60084e08a252b2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Tue, 11 Jun 2024 08:49:38 +0800 Subject: [PATCH 033/108] =?UTF-8?q?=E4=B8=8D=E4=BD=BF=E7=94=A8=E5=8F=8D?= =?UTF-8?q?=E5=B0=84=E5=88=9B=E5=BB=BA=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/SourceGeneratorHttpApiActivator.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index a3c6adb9..ec42f820 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -52,9 +51,7 @@ public SourceGeneratorHttpApiActivator(IApiActionDescriptorProvider apiActionDes .Select(actionInvokerProvider.CreateActionInvoker) .ToArray(); - this.activator = RuntimeFeature.IsDynamicCodeSupported - ? LambdaUtil.CreateCtorFunc(proxyClassType) - : (interceptor, invokers) => proxyClassType.CreateInstance(interceptor, invokers); + this.activator = LambdaUtil.CreateCtorFunc(proxyClassType); } /// From 54ed517b55b3665ff8ae02d37b67449ccc6692b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Tue, 11 Jun 2024 11:11:30 +0800 Subject: [PATCH 034/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=BB=A5=E6=8F=90=E9=AB=98=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Attributes/FilterAttributes/LoggingFilterAttribute.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index b1c7b7d5..d70eb8ea 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -11,6 +11,9 @@ namespace WebApiClientCore.Attributes /// public class LoggingFilterAttribute : ApiFilterAttribute { + private static readonly Action logError = LoggerMessage.Define(LogLevel.Error, 0, "{LogMessage}"); + private static readonly Action logInformation = LoggerMessage.Define(LogLevel.Information, 1, "{LogMessage}"); + /// /// 获取或设置是否输出请求内容 /// @@ -151,14 +154,13 @@ protected virtual Task WriteLogAsync(ApiResponseContext context, LogMessage logM /// 日志消息 protected virtual void WriteLog(ILogger logger, LogMessage logMessage) { - // .NET6之后可以适配LoggerMessage以实现高性能日志 if (logMessage.Exception == null) { - logger.LogInformation("{logMessage}", logMessage); + logInformation(logger, logMessage, null); } else { - logger.LogError("{logMessage}", logMessage); + logError(logger, logMessage.ToExcludeException(), logMessage.Exception); } } } From 70197bf2a6191a4ee5171819fbeb04da7c83ee50 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Wed, 12 Jun 2024 23:52:53 +0800 Subject: [PATCH 035/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icon.png | Bin 47512 -> 16615 bytes icon.psd | Bin 0 -> 127993 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 icon.psd diff --git a/icon.png b/icon.png index 9f89572d4a7f6108a1aac7d2d396706e0a0d8f7d..f1219891b2e35648ddbec878574a8ba09195cac4 100644 GIT binary patch literal 16615 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

Hp=&b zG+tTHy?q;zseO^`3G%wv%Ac-1-~xm`|6!T)ITjJbY4PJdBUYFSmY(V#E*x66EmBG| z5oy8OB67nOhugg)iwAT46=(~E4+f$-I&i+0nn<7QfjjOBuBGz$<>?(c|6bG3cf||t zr|KmV2bE@n=aSqL2aaW_s=~3oQ@7)_;#gaG4I79smmTG z!DB?g=a;uN6f7%@^OjT1y+VMx4eqnn8!??Gv21!0a{HMGdWekZq;lnndwlL4X$3y_ z%4o;)%#9<`qV{4vd!;&YhO-ID@|6)smm1k%&zsQmV#0@KR5a^YpGccWhR(*w10h*H z$-j)yc!}pbM|clAaH101wYYH=mIq;?Pcx9k+=wf?m^GD7c}Fn*r0UZphk2Jt_|Y5~ zZ~n-!2d{iY#YRbMvI%MZQNfZ9s($kM?SYEE>7b33P5av`+eP`!0W4)NXwQ??1^*K% z=^Jt-XzQ@%dqDDaW`AkIZcV!$*2Msc$#~K`#m$zpsrO3Fn5od}ve%+wosS1Mg4q&U$0N4>$>P;41tR z0*VunpYH_e7o5?*tV@Qq3`npHM#!X(9yqzG?<{J}|KW6Zrs+=nTNQTm7ew8({U5UT znQ%c&QDs@DQ0?kVtC5=!S?Hy8582GSAnHW1GOvIS=DyQ^zMUg_y?&a!c<^h<=e5@Q z*2!I#Q;jI9Bbh0=Om@F6Ph4vFtZ`6s;6KPqfWH~OTxMG7X*pFtPG5F3i$2QozjSrs z57ts!F5RaBceztM`DTIpm_E8Lf}Z4`)bc?FkCvxT4!o4ld9atXw? z5-Bn%3Mncn8Ywy{1}R%AX8wzM_UyE8XGSiDW){av`TssnI@BM$T`y1Zbl;xV@CXO_ z8wg#qp9R&0b41ida75Nca@eHB%S*g7zK<;am-zh>u%zjR&zU@d`Tny4fQ*Eqc(tf; G@c#j<^SvAZ literal 47512 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

94{-6R3%m5N&6PgI7Zbr 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 diff --git a/icon.psd b/icon.psd new file mode 100644 index 0000000000000000000000000000000000000000..8ba189d22117817639ca003e8b034b8f66e862b4 GIT binary patch literal 127993 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%0D+CW6=G{l82$rz> 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 Date: Thu, 13 Jun 2024 22:12:20 +0800 Subject: [PATCH 036/108] update icon --- icon.png | Bin 16615 -> 16885 bytes icon.psd | Bin 127993 -> 123215 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/icon.png b/icon.png index f1219891b2e35648ddbec878574a8ba09195cac4..0bbbc27124215661b845a09424c8481dc29e472a 100644 GIT binary patch literal 16885 zcmYg%WmH@}wDur_dyztMDTB92aT(kh+@Vk??(Q(S6sI^0R$L0DxVt+PcXxNVy!ZQl z+#e@dCo5+sC&{*FXNN-+r7+P*&;S4cri`?>%3Iz4--CkmR+ej3WW7~zPAXEOaPtGq zes3?R_R`u;008>C{~kC%TE=?-0GuWx4pwu|IQFq9qnmRua9Q0hzx*C@`JE!ZOM#j$ z0#D``-&DrHgy}Ed&`+{<863nA>s?J>!~k4MEIj0anDzkN?y7Pg5&wT+oIbf`&;@m> zthjO_C_b0gc7t8-`%3GI&;E(p3~%9M#?+9BF0+C|#UHYQUc2?iil8|HGMUx3mnQjSNkDQHiU zF5V{oyh&z9=H?jtJ(}aIorEydcd_t%9|&TW1ly5OYqyD1EAhczsWf6AeK{IuZgIx} zdQgnD78um_6Lo5vIG;!(tZhK;W_=Fld=(k~AHLeSZX9DiLK%9z7MT1lb5C$DN)V%y z;%z#0>2+%GTocR-#2h?_eyVOc0pFTt(;Q zbLXfWm zk_H1Yb40;tjOHi=Xetn}SP|rn2#IJg6kjbi9poXT`d^oZfm?xkz5ne@oeE+ufdiHQfkEXtFqWz9rjN};b z>z&@)@3DiV69wKwH(qjPV)g?_a=w}S-R`Xh#S-R+aRZx>o<~^pHu!q4GoQo)+fuqH zUoJSFJi=C;7|mrFLYDHT1+E+NPy4DG+$E#}!Y5FG6Ta?!FAHaNAvw0Zw-{AVgM2}@3l!Q){H(2<{ znl6Z!6kV04go-q~S_-kR)How$QVNCsSMBbV_I<-Qo}F`@DN|AvbJ^i^@^HWQ=EAd6 zr#s$O$R4@U65jQuc@2B6+9$ z!-zxv(>`bOB_gimO-I&C^|c21>dCC;oPcr(q9oA6p@453z_5+>RI%^Z9iP zpbdplN(Qtoga2!a19}T2QVR6*#6msYy6*vER|4-c0fc4&!Abb0CBsruRRqM|F{0#j z8D<2h6M7OTFB{A&G^9j3$Ya|Z%^3(Q$XN*)L-!u-j{#1pk!xF=%h>G=Yob)UDj*pl zP7!c!$6!u*)X}AsC=Zk4wU7qtTH@0LTXxvmXgYO0dfy_ELq>c|Bqh1hkQAxgC7?ci zHE+FR@$B5Du=|kT4!-^v5f&?H@2Q~`oEq<}EK*N>m6mvUW!(*RT^ztU5g=aY>UMu* z@WR3w3-$|$7u!W*G^O6PK-}i~tdAci5toRM=5@hI=NBp5C(90Y!5B*S` zMB}y}fG%WEiio`_ARKYK|IIyH$hT>YVl`%QyIyQ06lu*&@Gd|X?v-W5ENzSqTXA&l zxe_H~>rsNfy}3V!GVZ)cN7EST*I1U_`f|vA-f(r^ery69_=qX%*l5rq$wZYjK}k zVZe(Mi2eSSCN)wh`*z)1$UAZjeR#EDG1Sk!EtHi7<{}DYARwewa@57)Z?^5b#6%Yz znGVzffS@io!Z?+QINeTX1*KAPIf0Z95&7>)1t*;d9t6?okaBuxv+}|ew|yc@+^2fp zG=jaf0e~knh5t9(neDFP221pg74zPB1|(4~3tw*u@NmXn)~*h6N*0HoeBajyN)$t}K=!-y zY{Gs$K3BNgw6p7U8p1Tch3b85)WKd0=WaSP%r@_6yH`Q5JO=2HsiCMS5;h8P^85TN zx?Nv=y+-q1_tLdD%|p%f6&h@uiCDY8#(6mk2KYYz2KXLN;E?FIA0k|d5wDQh$C%^h z0BQG-$4_n?PJHf0Ds4L{1X?gxTNnJxckX0crlQw?8Dm5S80*5`EQn7fCfga48Q( zn+#3<0z@rus+kEb_arjVw&8e{GWZu_XD}bc62Ij$ZYdaKrl}0B zocxApcOrgAzD7z-l?dmh9De<2P%J1u7B7LFTlse#iI2NYUZj)t#&dB;vf;k~vt&yM zz0Huw;SZcFZPc}MZc%qjh9J#4NWdZ>-%Ot?*~e!SijRqPxo|4}(R@xnTbP>)Qv zp6>(}TXAx=cKrjZxp^{#=%?=4)uC=x_2SYL;(%i_6zyVA5=HY10{&<$(3OZFe}<-& zs*yFHHsBAXpw`NMr`4g&ozR6Zq2`|1%94ob;79K-wU4-Hlp|DI4yOE2D=vRbwP+p9 zCqb0?K=_(6<_4)aXTOKNcrIn=K6&{#DYt!eDOs}=nft@YM}amyGgAu5tKDR73n89L z8-H$BfCdS!4t@@$yBsCx4-Gd7j4}_F6|E42gF=M9jHRbiHy*)kmj@`TT{p#_By#ZH z{lS92cCH`3twXW$4{QDU7*KJtF{qDj)FgAF(A_z$>g$tzm-h&L;&yGacd0?pgvc1^ zSmflwa#?MnN7BtP39c9!w?FWAIeA`6&RJ1;8eNxSxl zUL5eTek=m9RjzB?0U*BPJk|<`$8S2Eqhir*?En?$&;Y_B* z#*m@nEP$!Z|9Dje+K%XyBg~C2dy)aK2X)Z(D;Kn^N%lnb7YPWzkp(-J5_i}lYpxpv zc~cW2Inuu`qw%Z$xaBybj9VA?dhq2!O2WgYB^Yf|WQbkwPVdh$mNu1nY<62@tMGxa z4pyy>m&GCOD($se9W{I@7p`zzymc=8q_Lw*6S8KV=U%kyzrp&jJ8!?17(xt!z%nh> zuW=!2(JOF_QutRnHMPc32H)H+qOenLeo?X_&;(PW1T{&(L%Qf(YgPgX@Bu1`yA>xF zjM+&GEn&c~bu=2%Dp3_G7_O(a1Zjw$FoFTD!If=+PjeG#WwE)igqOjCn}3Ebm`U%h zGmy10n#;6`X+m|mpwi)D#h@0EuZ152UDXJ?>C*E*VIp4m)IqH|l~XN6wHeGIeLbQK zmWJxJQO}sSk}bVhWrEjpy+!)>qi_zwJS63mS$8q?kzU=KFn=%F-~9L6o!ge|_NSem z<)N%Vv_sn)^=65+CN^q`P9N!ru5FX{tT0AG3|@QxS;X>r!i{*Yt9wH!ITbrIsF)3BVpVs>G@Ms~%Ig;~+$-{J79dqLi-dVhjWW{?_H|9rJa zR9H?()WDyQZ8k50rU?HjM9qJnrHpHa2HfpC6Bd=R=dz3C+VA-A;d(d$y#NR3&4Q0O zwkhR0Z z)a}iXJb?~si`T%}cwNw-)DtKm^;Wu4B8Z6N=Ed(eB@?)`JI7W~^5&h?L=(epFrw?A zb0BI{`*N=Rw`F`_oeyAt}Uoq3ELksP`U1)&^f^nN= zz^I63k-zSn8D!6POYcQ3?tbPcs<_pW-gZOKR`2)?DOQ>8H#q2opKGkweZFn)#SQH{ z*!xQ^EHfq2DUp;>o_8j@)--EL`zSCR&26ksoX%kjxN4?l#==>!&|syodX>?Zfj|P= z@i^4quv%5HBJ1ciO-Q%?;;hlHGZm`gYs7>9p&PGrR;48m&;0RRcr1g8 z%E;b5_j{_5_E^ukitn_}be#s)(;OQRaH5jLA%BiFs}}7Mu6j5h$9TF3e~glDIgVHGj45_v*@9;XQ7sibH1#mW06rbEA8B7;8^W; zeD7w3qrt%(Zoy@2+Trn-|DK1D=NF?8D+b->13t5sEyR6s(;seG&WP8U$@19%xGmbH zUzSKPp((sY^^mGZ#dSS`+L*sDa}GpONN$ZV*SatbX7NdJ>JB|wt} zBvTuevr5#8x__$~xC5b+l|Br&b$^I!);itr+G;i%$TMSM-dHJWh~$0bkuAVe8|;AcyAKCNi9K_`nFQMycSVW)xT|D_IK{Ua>}d2( z#`I{%aKpz{pD`-RuIauso1M=keuV|D+Ax&cd$EoM~nw)R1e* zuJ>zPq^k;rM58exGxTyY_AaS004LoRCR+~BZyBSy!&tP91V2jB;CEM3^Zkori8xWQ zu>HB?C3PnazmTK_TN%KbRvNwQoQc;63hvuB_82I!*5y)B86-kMmrQji5O(9ngr;eHNX3l&W6Ej&+o9&c9{G*Mi>HwE;}5j$l=r58C=DKJ zyLvu*TaApU*kT#M5vp{;69+s982ishAgikIg3{;^r;(i`TpP5F$s_!*mlIqKX*a)c zXxxob`mMOJ1_Gl|n0bKHHK(bbW0AY8RSEP}$c%j$jZXCD-x?&hBw1#taa+)6d5{-9 z^BIm@U@Z-ZBioueFAtYhUfWIM^*4BbW>u0Dsu7eVCMK9RN+OUK6IPt9BHZ!#>7pGV0yP)in&uo~j)*5oGM_(MkE@ z7cUvJBpL(9#~{r%E~h8|0C=v`E$AqE<^jyZ7bu>#V}3P_0pWt?snnV-2!?CgP6{Rx);2P=hf9+ zkLyKV&@m$D8;iz5RFfX7&y3;{Y5<%XZKS{lyzzo#Yhes+?7x(zn4zGTwDAV_!}~aY z(L8*a!*Czk57(@_W|9-~J)57^XK%A(q>5(@jBr%2;CJs@e(`#4Z$iIHR`ruv_x8k9 zUuRrY=2sHkcriN<#k}C^M%emTZDkbwj1X$N+M}rUS2;Jz`t!W;t_(1eG#BE_dgL}= z&Ej_)GtubHHpjaKo{uCDd<+f&t5hgLWfH4GaTabxL{l;&nq%I{L|Ipxy|Viu`V84>bvp|i&qNX0uRX8dRad4&fN z5F-?2r@f2gtfns6E`}*P%y#$@oh01};rpjZY-?pO72w;|;yO6SR`0g7FJa7-+vI)n zh4WN!yO%HKLs0D-YhP+uT>n>o>YqBrEoSQ4QM|mo?BYDoN=u?}lyp|D`-b!FCLOh$ zkBj21zH<`OI71L6AJR3}v4!A~zPL0a#=J86o>ob@GVcpyv!4*uG+S2}Iul>W!OJgyT6 z`qzTdaVAHOSqxPaO(Ygb1)K;M5Mi__Y!~Im?+JIv!*^ErGErG5m0r?Ew7wK7gt;qx zLCo{KhTS#b=8Y1)>L8rqIVZJ#gzN{;>b+H{ZEkaLV*5$*A5$&&&k2$i(zYx?FXB<* zE7f7QmmROKp6Bpwb@q!s_B$=vK^)k=c!R1n#fO{~z7*i+TEQ_IB@!{Po=~vGi2|vy zF`{b?bpV32jI@Ym4ZKS))nr5>ftD-5{(cJWh+$%;^h*(}wD#yJocp#dNn^OxLGty} zW(%hWJKnBbaQaWcb9YoR<)7E!DygPZ2WYI@=HO{_c!hM^Hw{Gh0``pNT`|$K@6nh~ zLcRrqqH@{SW#($nKX!4mP5yCj+V4Vb;{gWs-fX()Z5dd)Ow zUJ0+9b7pC@(0(%px8C6&{=*M=^+&;-JycAh!GYR3)5FR|npT$<_1e*hdT7Wp+bElm zntx+i1V!)sWR>KZxRvTCdku8&apvuQwbAHe6Ko^}_}>>hBV_{kPNnwgn}`-a$klhY zrZ(<`*R^2pjIIh@Ja(_53ws^?^-?{Zt1tl!MSrTy-B%_0x_;wkmKJE33hlY|roI+p z&?RFTFq-=JJ_yu71^7rfE-7ms9DijxFBADU)3r-}1ixQXSdeKsquRa(4|cS+?|EE| zcPG68JK_oQzgHMGoDE4bzm6Hc7F9M8oG`TV6rIYQOf8PEuLNZX8gltXAG>zX=1mcy z%Td-%&#rpoT1P6fPVo%p?7bapaVrkHW~sQsxQ2KtI5@|g-WdwN_%rpQfCsU#6RWk{5IQ zMd*$xv3JD_&2|Pus&`#PIabx=YM&v0OWfy3j9-0{-Pi|Te(bsa;ja9(lkC#-H=Oxs z>7Kvb?fh1|rMmD?83Jpxw>FX_4v1)23z|V90`j~&xcsff1BJ<8X_3u1#c`KmmA8OfjZ8hlu)nP2_3(+wqq&S`Era?=1{Z& ziGA@OKL@25G}rHX;JE%S_UhjTd392LwiqX5&<;aKVoS>BH5k1@x0?qvg0wYOAy$dBZ&98x$kO72HB)^KYqwB-8H ze354G$I1XrhqLef0>ye!u%3A zmzqdw^D&np+&;g8QAU5(vcB_uyg--C*$=s=cABbKxiIdA4bk+@6?efO`u`2(MvEEv z(&1xBbW9Ub&{Bg8W2V-oHcGB-xav%-qMR~~EjWEZJE4VflrCEc#}0ghr>yT zfy7fhFi)?CP4l*A%rqc(rGwP$bJ)#yFcRQeZSI|!iCpXgy&`H;8L&MS%e0hX9+4jr1Pl8j`9^+4J(>H;;IdVtBmtd7{fV(b&$P}1r0Cq z!K;B)p_t{!6e7MHV_*A_AG)1XTk zrRUvjeLjX_@doM~4&IEq1 z-_OVrMFhK@*@1q}S|}6e==gJSGC%IG#Xc;FWN4BZA!)V%&w?5L&~x3nf8!ov+f09m zSahF=U_`*f*$cPw5&Xof#A6ny!jG@%lfltT(C{8Qy6BwhqnPuN(K=2kQe?TNwpNT2 z`i>G#F@`7X1}3Jrp~&fSp=YW1@Dkbe>!x>QXj=c(_26#*S>9~*j~hBWA*4=j4M;gr;8V2WRN32ZsGH&M#E`gJe!(2q zWqi$nbCyu$E+7!k82S7%ft3L3{V;aCzj}SJI9leh-F;m`$=Bm3j9Bzx`(swz#79`G z!KownjdDI>qTCqqk!0Evt(*%+$`3s3^%V%$CnIc?zYWFb-g*Qw?+R|?{!o5NjmTM1 z1qcqn@+2}GlIu$eg;7O_^vtvSrUz;mqIFjv7f|FpKTS)3>4V9V^&TgR2kZI#*kde? zQ*X*CfAx&B9!43{jY)9uCVj#%Z7YfLn%*^8hs9yb$nsAP@c%AJu#8}-$dyAoZhNl@ zR)c*-(id>85zu~MZEJ-a|K=vQ%mg=1{Ho?{%V2T}E<^}iKmas0_VwBMDOnm!7X{oD zP`t-Pg_|?*b1joozA9~Sq`Q9koqkH}rqM7ZL_o^~nG3yRraMwo&}jPbV`lhAfTs+= zw3?ERK;muXr?#np7bXN1_LVr4d0re?BAU+M6%h_P+a~V<){VoF(P46j4j5cfJGoOj z9PHRf6M#nQh49@4VX%!iNwcQO>CgPyn~&LqT=5}C$W+X~F9)Ym@02$ZZaNpm2?lG3 zj@X|93zu@WZdR)SE{=-bI|cse$lSfdMNF_^LUH6l2LG~8VetK(uflG|b_KXIRT`w;@ooJ@g zYZH3LxR(o^6IDHwx7mld?y|0IcWZ}&8;XjY&?_=&{~$%O275ljhmME;)&1f7YpO8* zBig_UMpuFCHg$Jde|j3@klm7mA1?Ori;392;$%ckF)h_$W7)$D-R4hR<7hDA)) z%}*L=56ygG9KfXk_=N_G%7~vSfZZsrnwboj*v7`ZlaYOo8r+Y$08z2=H#l|X;F{SH zyl|3{n~XSe-00y=wFU?#f$iwHv;qN*3V&iYhaZJ%3!nECJ9$Sl8t8_QGfyEBC;PWA zds(?k=*g=<`qlIG->=KG3~uyeLgRGHOz6hkQ35C~;v}caWA6v3buunD1P=&+)0phR zs`WgMkxXbizljn135I%%fCD5TX=;n_)#Dq&NFs*GfEGbSSW}pW^~L;{-yOZt$>d@$ z+Zb$;$k{@1>6%jSu3yQaDeD=B>G)9?h+fyiuZk*QR6KS2io?7lWKa_t2-C}ZV}Pqp zux2i#JXWx_E7sq$S8xM;s^p_=7vN_kfrT=iEFQbaubB^h_X*JW4f~*{K?PypG4z6G z9^XE%`t8x4m=|Vrkk6OHiuQxw3HJN%gv~I#t_|yt4Qvgh1nbQ!`NlSXT?!;Opu2UR zFRqS6INyk63$dqk{UR;(m_bK|%>7*@A$wmNpAgNgr#JY|#ixQgz*E9i_bLeQpRPgD zYh~m6YN*;WAQS7FiYZ;0BgJYbhh1N#;Rb6w8^M;N}O2)^EKXe;1Gk;wG{J zL6D^l`m*KxJ=^tHdsbp?5Dzj9Twy{zM!M_9sRMjAwv@WwKZ9R|BVYHr%Ze#6=aG_L zEPno4Ae9j#k~_lu&3=F&yHrXSX4OvkeGERV*yo*b5sD9hN^hPb?Qy6Z z{`{v)e5Iihm_z_0;`A8&{AOZ(+Xf?L&gURpV-c~0)W?THhA^gmMlGr$H$7w|my!0! zyZbPP<#9zS^8tJeVR(Z_BKm4uj_dhsH>v(RI+%!iQ{-d1h1WmJI!NBV5!Fs}FPWT5 z1PhC53o_;$A~^bJehWwGgc$9t*ABb(}= zcDf=T^gA^XpW#k$l4$$$kuB;e$8p96z-Riwk2B%Hl=#{Ae!#;hvntVb6X%k6~^ z)Z%=`+38GP#GyD4igHSf=@=POh(?aa7|77rU*0S(xkskj8SB0bb9_<%pAC9XeuqLW zNAk*}gQ`_v+aGE)nC2!%>zif83V2X1$jH-L;2#y07>YMfY6+OV&McE=`KD@RyMg1z zi@UBN;oR)scaJ@Rkj&gK@E|rtD6(n|16qU%SQe4i-n@URy?iGzAY%f0vqyFd#c34^ zfBgDGZUi?TL$YlKRuTU7EJ;#!n0g(%7$CY-<5r&K64O{oRzbt9w68}@`B?oj;0Eu! za3UAcQNYTQL(4?$fbkIdB|*BvSm|Mdd^w%+SInMhb%PNZU+C zz%{;!>8ozv6h+7zRcT(P{uGhruX88Z?NM873#XfD`TELwEFC9af;bjWAKOt7SwE2t%*9l-DL+q6W=nAr4y61S=> z;FXz=GnGa3yXZY$ajdo)z6Z;u7&{`uGUW)3GJi=VEj_t`sW@S9V$q z=6NdE#^}9;hFmpBXtKt3(o6f|-W)E&X+_k80H0pC@qw0uenR1Zh_3{V4zwUc^bPof z-k%c_A}ws#|E%2thx<6$uW#BOEPmwL=Yq{cFX)gREnb&9IY7Q&}k#NmuXqdd6P#ec|ans6nun;{8 z&@V0@AFbNlbR5--^PeT^Q%7G}fF@`FH#Z#AKmILO@UQ`C4YkOLR<0J_GIU;cj1EG? zj}fdZ_O)dgRWz3F1aVveSj-_pbfT{LMRsqWpLB8FzMpwLy@ZH6;hk+(DmdE#&iOfo zYd(;B9;`C13_}I=e}Yvog{~X}!KqkY5p_!xFq^;`PPrBRxT5Y}3)TNHa)RJx+=lVW}Vs^{O`+Yuvj!l_# zL}&5X1o41w{#sxAv+{}%229m2!u5@#IThr=NboLAOWmFdy7t%KavAcLhfxxDON?VA zYco)O6~V4?DbvwZPEoead%Qk*EAQmInM6T!W;0Sv+R!$TUIrt*Q_9aD)k^iUOOJ27 zMx&Te-ox(SDEC<&R`GW&QS|OJR+t*C zjM`0f!i2`UMgi#t297Xh?1upJ$yuccMDPl$g$yYk&Y-8m3_w%zGz+u zKtHjb2=H7qX~G<4*7hE8dRd{|VyFau5OdVr40vFw{7?~8&8Xwu2)MA+4|3}Xs|Z?m7-rYpvB?X)RxdD#&&d^nXmc9^LERY3`vnyu1`U5=w z9ooCOS-y?GkaW5;$b{ypDAd(EDyP&r>lD-RP}hd>79&B`dmeoCuPtot9;vt$RZ#nK zKwTMQVB?40Ax=Q+T5f$9$LOYn;4&&zl}_*ptEf(D6ENsBtOPe)qf4h(s?F-a4NSaX zQb>A52^OWjga)-8%}8XAK?A?1wn1diL6Q-EN`LS6=?niU!ow1--#e!$T0Ju|jVtGZ zP+&E+!+mtj!Vgm8y}aaK$V6B);xFy(QXBy;LOy85L{cWxQkyuV<}{GPycuV>0(Q4d z^FGS`bUw>eX6by1MSQ=7W(gMM56xuUG=9#zS~3ODQC{}FgA}efzf@8YI|DC^enXmK zk^9)?5y!UaQ%Z$fjp@-=opf zVaF&uGWa)7V2C5ie6J!-8M2LWmCW{XaVaWx{X#1K6i&~;aDD8URBt!<(IH_N=H@yJ z}*i;ZT+1YFV$hj#3&#Uw+J1MfN(2+`!zuw7HFx4sbL83q#r(G6>V~@ zR@9KZf5o(+H0a>n`%TXH@)R)vdC$9K>VjZXkZ;z1l9TQ*;k$kVA`MJlH;$702w0RbMLG)l@7}Czb#?VB z|Fr74$q}wTP44W{JvAJ9r>NaT80c%?dktqJCAII-4j$5@kMEPV_wxn2-Ygw|@_HcL zbmx3wW}Gxa$%?$Agf$7HB+YLYw*fO}bF|_RN_#dmB|Vx5I;vlOMTdW65{=9El!Pmg zi-oAX1fUVxz%5%up0T1@C_TL9e8x`jrNqoG?osXmmn<3fUSWQ{Gr(~xTh>nqDQ)AKS7jrc)Woj4{TQgKXMaH%)EP*}^fRfgaHa!`NG{s@c9uE;!0AB+SUXdztygczS9GWO^sE+1hAg*Y}`#--S zv;>?e3Esn4&}7AsUkhUQZn1uAGXAV5Qns>CB1_gbEsqFFMM;^uNW(07CN@<^zFa-PQx5*hcByCFjfu&h z+3PdMgSLI_hFz1J@oPger$tz^9-ODuF(!)F&eL~7wqO4DOTqjq0_j6+6vbq^uCeP? zdtW=7Nic;!>KOJ&z!_2AhM>SQSn?FRKNOV_c-OJSXn*u4cV~v>RFiv33jd|X;g0+I z7MdIRS8Am(8$AEC=!o6_-!^BA@qI!L_TE2J%#{AEb7*(uh2^1K`8(4y2Kxh9Y-|QT znK}N@OS+HzpOy&K0At!qT*kbX*7OnV%))P2%y7%*Xy}tz0`n4?H`%mzeJC7WqV8P~ z5JOUCEKs}Bw1>(9s7`!kOG?>~4MX1ZeG9MU$r=@sH8U)`%*-&Hl2R{C@d6 z#V&hewVUr?T!$T#|H%M{$OXdQNYshC{N3~S@(h#-BVAm_>%Un(!}oRjaI^OD&LU{+ zNRYWCBuWN;)cA^1ZHF+$k*wApg8G-i0#XK2+WXR}$~5ADMunvw{qy7|luP)%%n{Ry z(#deqh8bL@lMI@)8NG#cClfuo2lDm`*+$cfxi%<-pnuz!~-nYQGoRZ zz@f6lGer8Qf=f%c7@UbNl^(H=5@+1n&wRX4lkoic>7$QcB5IL2%>d6Xs_7b{6Vm(B zw@|F=x*F@(C7qQy?_Rpj5BzMxm`sMRQvv*BM69cktVWGz&Xdk-%FGFGn4pJinDx#_ z|BE|cb{A#4YA-suC9qL!cYz$;CsUX|wY~!kQ2x(Tk~DuxLr(o{@Y`&sH-OK^#ipXw zw&f6$`Ju+*q*K56_F{>?`g$p^>SoK3x9{TH>xY%6i_?z5O+T#KxmNBo5)7i@eF_jIrgq&0xBc@oDm6x8B1Uix*^qgv; z2ynuFrj=(i5IE!g=iL6E_oW7`{Ha{0$6WH4z{cMnyD=xq(}oo8SmG_39f61t4t5s$ z4r{lLBw++5EtB&u@rOVe{YU9;0xX^Q?p91*4CE}RvUt6;oyck?qG;$R%k2sh?btZo z_HF*VO^~s9mx)~!tmQpM@9wtVLvhz+|L{T~KJ0MVdcPxqYz7W_`ku9;1lEL>wI>0@(AC}eIAP;rV(*tEiHdwO1KovPXwr4+u^F?Re8nt>ZIcAs+A{oo`ZU*#CA5)T4zTHxBMY5uoNU97OR(FS zj=lPl4sy^t3QkgqzghY!!BAI8!4B#;#KFWZRn(^CS#uyRu``v7+lCwQ4Revk6>$C$wgzi&uG|_jUObyq!P=lP|on$fja1G0)Hj=jxnRF zrE#B(VuxJyk*wn4u2}~`KgK!i&2hHJdIOgJGHz}3_p2I8CP<4UyGl{KQ}Ak6PvAM( zAir&ocM5YN`xM_6VNzTH8fsdOdzxc__BNpizJr}kbme9>y2|j4ID9M|-y3ciD!z@@ zQ4I~u7gLUnD^%L&X(xi06{YmAE-IuRUtAM2M?%>Ck_{=iUYo%hnE1|-(sugc$->JP z2UU}y7c2%3?6zw$g1`E-v^uJx2_JL$-d9L?dw64{I9FsPs@B1#y{-QDJ`TPK3PcID z^lw~52%96dzV=lg+9Yuq<nUf7pDM3@tzzPegfL5navNnnSz2Cj}|W^2i%dKoG7^CR0~ z=4}u4z$k8bvSuJLU^YE1F7a_)w-ozjV)g?g7*r)TDIuQS|LJ9b6!A44 z1wV)yBGTBP@nP7nsoWE=i!iSLHTARDXfSk&ek{PS_Qp?DE~gGgzKUNc3@xovI`c}!4R5Df3E)y#UmIhx6EuIxk zQOz0`OY!f*G@SH#WzT6$|40alufyKs#Ha0OxF@Ncs9(dr^f?No2T4zLCL*MsQuYUO)(3*CcXNX5A zzV0vWOm75Rl1B$|Sxk*l#dtwXQ{N&gYColS#ChU-2%}0|rk)Z6LDav^;jhm7T{7O( zMX-4mrx)vSE0?B$j~u}#@@U}s5zz)_%dnWzR$i7Crp2%Zn@@bRxMA2U;D}GqNek%1k%Q>7UDSuJGVl0GdS9F9z z#fg{MM*nD4QJLg_ra?LNy#|1uWLXZ{dpQnNYSXcyuEmW-zkN9cz_-n)5l%X1ku zX+*xcm2g-61jb|oZTL(@wi4pBLuM~bwXx~4d-?E)-vg+lNl)eoNe~(QOuf>ur4I{%S1Fmz8G&gA#)h;)9n(*8Guf#-keQAzzin$rmntswO^1|w>* zp>iWHmmzJ$9{)tHw4)>3`jLtASLaRdsz>SbUwX7UGUyqVTElf zfrN0nKnGx;x40XMBB9LVkA}=ZF|l{QH|btR$yNpgWl#yUAU2o0Bcy^Wu#!N4I7YOE z@KaXwF!nUh_rxc)1_hAhQ<`@eETKR?Be=Vu-?N2^g84+jG_~N;@gIlLAJ>hQag};k zV6*He0pBH##Pu0QO`N`b81DfyiI#i+ja$^23iNxFCVIJEbw1~1l$QDmP^aDdIm-Oh zl0PlcMP&7Q5T3Qh(XK06X+=|*Z-z&WJK@R2X{jx+LZTc`T<=x-ie3SGbBhDw2wf9IoZZ zm#i(eUX7tQr%JfUOzHObTf%Mv{JuuDQGX$z9YoGqJ*Sb)JOV{wqGms!4MXg=(#KUU zxfOQ~M?(~Qzp?WE$)*P~J2d5t`AE+t?R_~Q$Z z%qSvSx}oX2gutzAmj4cI4a-gO&=0jgO5!I3>s=08*P-;gA!o>Jrh@U1n)ex>5S1>7 z)O*iKbGJbI&ECRoSb9=a_<} zM8NXkizD?9N9y`+4bJu~Lj3N1Yd9}I2^_T96Y2z!pBoBDgRko=_EA76$~v(a@@r}+ zV^8TXc1U_o)A*B53T{k0nQ3BB?a4vq@oRZ0aWHLKm?? zTOB36{*&ENIT7WxMa;e1doWU>R`}Uokl6F#JT>DUdqW@8i>V|;Ck9vxJL#l=p&$^L zi9Si~N#(rfW_`O2W`;ugnR`eAq2Ln7gDAowZweQFA4uSQ^?#C!$8i|(^I_1C|DQ)( zK@V)eqP77pn{RoMM+q_Bm_u-tSoTCvUGfyrsxf;G^dR6baf;R|#RH7^|0ODanfSS= zE_@L%ddu=39r@go>N)S}DDIs9@|UZ_JstH5b`&eS;#f4^(v`(%EEmo~4AIun@Cn~K zTSp86D({Vmf1XL~R+*_nz*13s&p1Dy?Vh1?;7c%lccD`5+gEiTl~sq6Ww?0~PT-yx z+FcSpy*0|HY90iExKQQJ9`7K5@R9gxIQt?B2SJe7Lf;iHEDG|_&WQio+Ba3`ql|kD zJHU@q=*%kPx=GSEGRVFXEx1f5@bQH-*e^6f1w|ThL=1c7lC+m{`3U*w2MT#h&jVu~ z*)gJb{}UMj=KceG`WlYj0Z*`?f)0yF)?XvpCmqoLklGLO?Asr3ZkT%N7rnpi1@JTYum2ohu7OaL0-F?cqY%k5$@Ch@ILSK6 zyz7GlcN9MJOWtcw{(|?@Q3*bYFMk8a^Ehbl2p#j>0+D?u#82j_FPUZ?1r?Mxku}g| zjAW8zlVp@+m1LG=mt>e^`8PZBB$*`HBpD@HC7DIpeJ6hI i9LX}tG|Bd;i~fHW#a+Ph_5efx0000~Dyx;xKxqt4D znLabqQ`OU5J@r)eJQJy`D1`?400IC2Xfo2`s_*6Se;X3Q`@TxMI`6#zx~NKt0+&aa z0^Sdh9i??#000!6|27~XD~AvOfW*j%i>Q0%ocUQNm<>4@b}cU6-j1Jg`LG5WkkCM2 zD1gC;02msIktA?p6OEj6sAxl98Z*N#`Ku*wUpG^DNo_y+TWxUhAd*~xa%g`#OVk29 zSrgy;EV`SJMQ2Q*5ozkr;1^o~)zC>bSc>tfBF27Tph< z2mNmM4MezG?bWaW+i;ip%Qr?Cq>tg0Js(L3gwo=D<$VHS^G)L4mYgi$nz|egwFsZq+v3Oyl33VnJ=89Y@-~bu4&!GKPWI> z(=VHs`qk`m)^|u;MvtJFqt|MpO^CA)c#-Z&TkS_M+vQ92 z?&4m`Z!~%E4*0~nMh|cIb?V!Nlfh$6a<---FDZ$rj#OrImqoj_>lQ0zXrEbGs+uBV zXhvo7xE{EG0>hv3nG%6#QqfPu1^S#rM`QakxRW5&)A59(?cWYKP~51|oeO6zpZ$+3ywmJJ3n;kN>Zl0@$oz zIN)3Kpt@yiAl_Bt+yMQf?oRZgEnH91p&(|@5+JR6XJ&ep>WRE zZNXdrh-w`?&H`N6a}|@kWBsHD(`o&j4!LV}84XPT6E$3F!)23Rmt}C9UF}d(2z6+k zZCqe{u%~*>$OEn@xb-iXZ)pXKe3^zc8zwYIc;Gp?BbJxuG5P>L;&J<}=pe=;(a$9- zq(vmoJ)Yu^pz|Y`*E!GnX67O+2gM+IzptygDR7rlCJ97ke@8L6SwV_yLx^G?_EKA4 z0mL4~+M&(S;q*})a6s@Mg`DlyFcpZy381WORhv!7=>Q_przZs_i z%cFy`$w_>$56iMNcSv1V(l3@S>aV zXX{cD`05m?3^vjl=XFav0j}2(-hqytw|oP~B}?ExML={tq%SH;m0oqjL|0+M@chqqtO_4iqF-6Y%bnW)DUlrvGll0|EiM2$>da%|x$qTT+1PHK@Ew^MN z^sKp}g~MK$#9#3J%HQz9-R%2fEc&7EO2;1W2mDqcz(eAam*L=QIRWmhFA;DqfL%AW zxwAGj&I{or*si)dfPMNcu;kGaGO^Vp28jxm$;%np>A%yocRN~{D}hp_(&?@xg7^1FhUj(AsH0^$KW%FGp~5j zz!Upzs#@49yq}EJGdOl=ivN<3IDUP_A_)cV?aenNiiG;!_)w)Y9nlWfl5W zz^TBFA3r(P8~;3S`STjw+eem`fw!HQhaG|?5_@>hl-T%Hn9Ck|3T{LLVlCsqIUn4| z@y_eaO$4*OX#?-em@%OWK>SZHp~>^#L(6{N+wPOXZRE|-PT@Hi=*Aaro2Jdce|qlu zPC^GHJgVjo-2+ZZC;Mf@3Q4ML5p7i88dSK=gnfs}Nbj`)@%ojzz2QphYC-t{8%)oS zYo9Kc&OTjk=`mhTw-R|?>cf-q*v4^?TgsZ?KJgfsiK>TU)Lw15NYJ55Jo3x81pMuk zcpOBG+AUQFAuaj!kv;PCd$MwfJV}w~gynZNgM@vx@W^SbM}Q z4d_DU6Q~o*1Po!10C@lVSE9!m zj~&4u5kBfch>icSrX+Hw5SM+)TIWmP_b=AY`d9@w+0yYd+)O~K!_33QBS<&8*BDvw zUtj?KGyuZhtX~-7jI@t4B>G#&U(BpNgc@Ptg%nEJREu^>1@%z0WQe=$&U|;;DCB5A z@@3&w$V&QRSgahV{MC^X$uEB-5F_tk2B~iZ#d->*2V=?RtX}HZhAAw$k72-eMk62|;^o=RueB&l_LrifHi_vCxj2zcFDgf%w8TRAmd`Z|1b zfPTSbk1Z!@cX51s5anNA5f$n)G(+_8#0QFZdGaV6VYWd|wR{VB} z*%skb2MF+sG35TdH>3(y*to;Ugz+{yeKC{697iab&u|zH^8j z`y6psB*;(`f1^mbMVQQGL{~oeii~-OWO129tL>D`Z7Xby+0*>H$f!`n%d0o7aMzF zuwn)y(8l;v{b6%f6L4YoTjiZRfg0rIxgq z{zqOVqtNgVVJ7!1gW5EsJU#1Im$6=aZ*2`YY%M!nX#?)v`lT!imK%Zm{g^cSib4D& z(yoM##$WFy4R)xk`?)~ElV8CvR9{ZnF@cMBFyIRMoDpR4EqQ+%`6RJFVPxj=g(4A= zvJ(045+!1R0Ug|gB6(A$UcK%`pT@p@XFuHCwoP`c;awu#tQpJPJgGhVLWZ(^ljzb> zokj~ehY%$)Y#R>T0#QH%-JvsA29-VgE2CbFuv$v<;)=XT)_5H%z7X+OHyBkUlEZ@w zwvGHF;d&D1M)#xz>Nc6{?jLwt+Es&>I1D2QY7l+-YP-9wQH%W6B7-`P&!R%Y+BX^S zrcptfm4iQ})xc5#`Ki>mf`|4jwnZDO%lRYn3EvnanA5# zhuv!0I66#ST}iVs<{52B>Sh0PrO?~rO1aVVy3nK*hZz{DM$1jy2_rxrr6nAuy~t?N zpc)_eKvp^bZ@k2~uPxaR&w={!-m^HO9mC0~B?C1BCvh+s2#=n@^<}{>c`Hd{|E9a2 zNv0^7wASa;hb`0mCSr|1SUI(CCd#?6BF*b+Tl(CY?L(8Q34k}#rmco)Yur6GjZ;%<@xzd+evRN0moeh^ei5}2`8K9bxe~9+B`=k_>5XX#iCnNRA`~4Jnph-!g7FVv!OPRy5~>$5Yast zxqeKj5lXp*BV;sge(N`0LxTtR)ZKaHLZHNexQ26Q&}t$d5_2Ph`X#}cNC#UhW)~42 z7{8j3R7>j_&O9WiM;gUrUQk$Z9ZQHJ;yU?uh zgS4HX_)CLnbzIZIXf^5RF#<&>r|~(g8a7?|g#BmkGId92AT2ISno0)fh)|XRUTN*u zZbEP&O21c8Fcs1yzJiOj8t#Eo{=RcbfnJib?EG5YDvkoZ{+ZifU-zob>j&Ib8q`&@ zhuD;&c$PA;ULc8#2dh?^Ofp`G_!~h*6GQn-e6`>~KzMil0sn4wLmLn{>n%ybrsgUw zBdwBgiHo8i(ZJiH%8R-lr=7SAX@&<1UrWNT*;vTwwE)Tn{aV>yMC1 zwSar-CrD#f*l2auj?A*8!|0Llw^F5f2V_rV_cmb&uH3%Ljv)rylCgurveTKZBc1v5 z_F0q?AAhBZ(UPW|oEzKGyH;S)qhvQnNA*fZ#>$SEqlk(8^F_sH;$rVOQ|66Mt z1Z7uO>Wg4)n$T3P^_Ob`(m(5l#@9{zF4MlkuSD#Mdw;x$RXnCDT;(_avh^T2nLD*? zUZf0$e2?g8Nh(*ra5i2q9-yhGC|Eko`3U<(*y-M~Pz$A+i)*Tb+ zsy~Ru*8C~WpJ|e>0ShTLOw&mnX7TcXG^D$*;S2Z?5i%k%-s8|bJ#k{OtpsAY_Z>c~ zoavk<1Kw)r`^_0J^6D3m(3X;v6Mm9sw@DR+DR%G(_1SZJU_g3`VA8P{ER~haSiJm> zgD>Is9hY0~s#EMf0%iqA8#*(AJ{PZUaBhRlDB5T0@-xR%<8##q2Ks%J7WjU7egxDy z;9VGpc&_+x6jD!K%rnO7NWG-!rNh?d|MZ+YM1%1ipyZ$Hmf zATQ?6u=bXo`Lp##D-ymoRjo0e@=%B6br#M%vK#Ypi}*4Ju5>$(iPkEUfX!XZ^;xNk9>>6pw=w5T2QYu zwDRs3?70oE%!v~!*JiB;p?KuvJ-sMI>X>ZT`jmne9hzdQthv}g@6QC5nn;JJYDubz z0ZbSnSuAO>&D%CXa3>I*lf5&$pkSNCo0^EKO57P$v35;Wqs6zHZ(&z&k2(?!WVGid z%;Sg6-OEzV)0FSHbM|2#JAG?q>w&NKX5Zt>o}N)~#oI+d-wKM+x7hS9biJB@@M?#y zYX$9qE8E)Jaq`}4?r!PDO9wiMascth8?jJ2K{qIyH2}70!;`GZC?Ia)#9I4h(x`9# z?Vqrfp`j(JEI+W$IH8!RxmEx82WdMF3GA>mzV~M^bg`-K1<6WqI2ad?gzAY=)N-e4 zU&LIa`+dCznNOC?&tVmV;AwOO+a^BDcQym@d;M8YA`F7MCC77&N)T0&; zPbzA`Ww!f6pdd_*l4$eoEqPMOA=l^dNgUhnmz|4wuNyQ_ZC@V674I`0)xd@8Kd>7F zo`Q9Daz<~|76x6y3%oHQOkS95{)n(4H2S%O$vg`M%ctamJ_Ok1?f*R*U=C#sKGR9fSz z1<_o#; z`NTK-f3XA^sa_ULE`$VVmHV-MX)8ah=#Y|Ik$lM+WQ>5v+R_mLbQE5o+WNr^;W*r~1^&BofPP+bgu&BNsv5P`=hvX&kxK{=_Bb{sk#l4t zmbm_C+e`({{I|MQg&2W00!}DlAOs~CREX^zsJ#*w6lT~HjbmPfn+-(%E4XCrFW3gA zOT}RB`yMU~Unczbx8pOcFpSjt4I4?@4v@!vW5@BDIzRLi`O80=2D&@<1!U^eG`Md9WOso zI~)fvR%Uo5&sCrOZKUC)!ajKS7+8Q8M-45^)wkfKq8;88isH2vSl{;8^lo%Rtr~1c zkOYtORKNb^kDKoSCuyTJ4UdLH5t3Xioqd-w-#R(*Jti*(Wwyd@9+DazqeqmRXw-Lc z$p6Bn*o0%(6wrR)W?4Zi(My0s$9j`%`Q&X6C%!>%$B&(1>-ccZfgnzktipmz&QAnL`>aknmjPS^-j}o!-VgdFn^DSEW@(Z zcI`D$Q&X2316;SJ%l??gDT{CHsehklg$z6x+hCT{^gWd z#hKPQ+}H>>s&{4wd76&tq5PI@PPbm^lJG1tN!dhE1aK2@26j^L4G$$USSC%B$Vzrj zGo*nB#$1?(4sKETCS4kdkWi>VxQ%CB>|fkhi?`(`-n*AJ8$xZof%c`rjyrUw@p!v+ z;O>0k)#4B0e>RgS3l&hp(F~7PH#+4IXH06m&2@}GYWxAotyj1{NLMi5ntLe8Cg^ki z&sw4}Npr*?2EJyf(;|j}Exlw0)4D(`2mwg7fJ!2~)sYV}B%p9==!BAlFwLJW7is$m zzxP9!EI#~|s_0O^mBrO*^+C|mVDY+8dJ*a%1r~jGdKx)<-4Z*(rbc5*{H5p7ziUNS z9OWTtY~Y@_B){f2xB#&)g_6j96QTqt4?iqnjyaMvKt<@sSuLP+S%+H01x$yOH}K_! zT4PEEr}t`S6p*hN#4-$r8E8o4!R zJNvq~h>K|;h_bjpLWx}0f{S5qdsO+uxU|x{pAmgNSk$p!wl%TxCV=pPfx;ZV z{KNoP@K^%9TY&ll-7Whp1V6dN-mg7gwBu-m|jtND=yIC}N;xW#l*Y2r{(6h`5?)VZ>AvJUFqA3_V`@`jYY|RW@(VJlB?iP$tIo=zwM0Lu`Cs>Aig~+lG^jB zX}(y(n-Ycg?F&y9m?}}qD~F}g1>b4-BGP}wQfbSpu?4$TmP*xP`Y3y~ z&$6+iLCd(lSC9y?Cw{S5PTnvm>(u4e8);XQo*GaJsS9zXaf6T7Cqa~7Way*l3Gy~N zRgpriQ@xmx6^fY+Y_sl#v|CK#80}bswsj63iRx4Bxb@R=q~T+)DG?wC#jZVC)vduF z(Vxj81my$ch+N?gNWB{R0D*CftIpU+pApIh*mxp(_x^kd7dn-hlTH+DE7m<7#EF0GnD) zqJ1Nwdtls_HQd1`^K|{nEZUHDe80dVp#l1h@O=$=2I-ovv?G3ntoyd3k!lJbmp}Ki zM0f+Z0^gCYxxSNFLj-Z35*!^Mf+x+O5~?s|CJ3$w-*`in{`fP!qpzYaTOw0~WHo>T z==v+5fCB*i2TgA&V}dJbH50Av9xGm7LUvjwa|f^BR%#p6cDQ(RisFvFu2?~GJ%Tsv zEcz+Ksi0(%5I@JaY&PehOLB(|9LYPSj|7s@zsd25{Om<8Z+2fX@q7q*2FiP>PBeBG zcIJZp+r=_P9`?v?T?1LZJMXG*XSKbVbdP8v)Teh>k??h;ADw2_=_Diuw&}o3$&6V! z#3#(q0VTNU<>8O{oQ{%I%2a0}-7iVA$rVH@OCA7F((Y1i3>@g#BzPd-dg73swp6%jh;(apUxL7+oD!Na&n<{l~X17j6MAV~p{Q=60qY5O z^S5iP1+eCGz`DF59V+Zh4Z{xET^5*R?wZocT8hJKNXt!Kp=&VJL#FqIpok#?6<0ko zTFO3+T6SY_aS`CXYd_*M2;|$!Pm*H}8y7tEG*-5R3uHN(KD$uT@0UbtVfC}&q=3^8 zI4DUJ0u>om?L|kP+%&I#IzE}rvb5(rYr_Bc;E=rA;%R%Mw#oL3 z&Pf2%mg4XiM_wophX}RCT0~7Y5@Le(MZ0{LTRWj4I+s*0=$j)yPG>D1v~64iD%u7$7V2pml|l?=mxbFF-SPq zw2?=sV*CuUrZ-iy?L#*~u~!MzqlS{{Bx&nRpCuwTKo>5SWs&c zK6w5M+l|wXjY7e%BJUq#%XQ=1B<}H4W^e~ju<9~0F-Q5D?DW^ZE}ifn0uV#y)^)C43!=x#?`{tku<=zekPT=fljfH zkH1(J*aGcUt^l znJ>O{-~IeY_WJ|Z=0UUOI$fUa#tTRXuR+sV^N0H}42jo6ILBo^dTcpG-1Q+ioe7KS z!!s&nzly? zG3Rt+#C04bzQ<2V4Ie*F2~5*2Gony&qY5H|++@9BoNq-hD1VkZNPyY@__H*tE(Vr0Cp1f+gpVMklh3ST?(Q!Hr zoWf{kbj~IY&2x%>?2D<4RAmd?G6|DaM@|lfOK`fg*tx}$bE%8Lhm$!^Jm3+B1F072 z$OQV2B1~roIo9w%l;-%(gF&C_9Ycd2KE=N<Le>vp^QN91{oNGws851)c^u|ZZHukf5oUfVT zDLWEuJcMd~kJg0IdxiFx_)N=SH_`M>#&KelCh(+Fdjt^fPi?ecMD~1XU?i}mP)3Z% zm%4}!YEv@Ucv??jMwn}VIfZnJe$hKOIU3JN2T6Pn{4GLD?azH$suVa)wjr||9 zTtR_MSzkAdfzzA^heL{UmkVI-Sn!w;>}WBdc|_&yDZeQQhMcxz+K{J7BjSk4H8G*- z*Z@*Iagez?bdu5eyW{NMKY55#l`8OWjM$B)brrKs?JvNzOBPcsm&Orz6iQaWL|&ro z?Y}B-FKAlBz*WQ&U7VyVZMT$Jo;RmlvtF&wLxo24O zX%x-CuDahrXK9FfUojRwE=zv;(2#;B4rjsEegd-O2OBo8G;7O*=6aA?Yd758v>Bw<#Kt!SOQ~G+likfX~ zcP$k*^gXcXdWTgbz`u9mJ`pDLT9si@-)L1BeEdpI_;nM#f0ZuG4Nx9C_w`rJ5I8*@ z5J+I?H&D2YlV`C5{z@k6Bq&0PCR2*miGsSF{i#@ak-7HRfPeMFq-NE19gt73b^$Nm z&^4Wefp}{}>0mdmdNO5xd$rODjuo*ZUg)Dsf<7~tvTUV&A_-nek$zcb0rh0 zX1t~~EUxKP+%=eVlW2*6PSpe=*LSx!@KI(D@Ur=vuuyw;kRzxq3nQ{7oI)u8+hF4# zH3^PE9DIXaO4rVD>}CrX1osm%vl?=a2&`OaAGZnHT`9QhQ~t!DP+GbY6QJ&Wamm71 zcWVHDcnEWi+J-*;UDyeuJQ~@f#yQY`+WKNJiWb~ZPo>5)O@`UrwJA{d?+a%WW5u3KX(U(JI3Tb0~?>xqR^N zOPfFXbPd5uNN^0&T%(nybl(iYLAfQf-;nUodXvU++vJ0w?mVHgo;|GjwK4f_dB7^>MDeG--3J{ZGR&{byKoLNE|;# zvD?NR4Hx%uJ)!(J@|(qV2A844IM90@W_qUcI0|35E5>-Q&B@sM0e=8tu93N@Kd9{^ z`qJs`^Y{=a>o{(M;P$&pe+CArOc0y_j~iI0sk3g4*8DK30!R6?5rgzX=?dK3nBv@- zip8yUa@x{%bZYm!+-ba2QpblaFin@v1$+TM#J8b~2t~D!#(MWD87z0mXqwF&}O5M6SjF@j63}Kt3 zZtMjwGa%{*7R19vjI)VgRJh|Gsy&eZ-tO{{_Y55NHx#NO1A6E5Z|NS=f#+-} z$pWxuCF}82(sdZsO7>*pVaY{sAf)p27CjAYDJJPae`g4S;JYw_SSR5KX&oN{;>M^su3rE=qEWt7ngQ@O?E>%^@IE=2>#*P7wC%O!7`H~>;0;0ConQ|(N zd4)OWPPES(4`_L|8;tVT^B7=9z$PNyX%xW_*J&aTX8KEp`jqjGAi^YB|XptC(5Ab9BUy`BGa^k=D; z?Txrpt;2^ZUxe7R#r%+o^!%X43xA@tyeFLh;5|acl(57^wTBpUsoj!Q<%7SY`nPbkL)`pvDrF^7^Q!Jm)@tZN-!Tj-ou-b=7~r`bdwviU5Fik-Rsfb_P${CA+OTIN-jP#FjjWsA4`c#atRhRGxK5|37X z5&~EwkOB>kTxO+f^{{F5gN6={NSqaRuwVE~Nwu8&_c~mV~p*-dc_&bv?D~M!u(&(_P z)StzsP<&V371P$HN+Y{{#EJ}l5T7y}wX*C1BF+8W-Eo{RxpsDmX8ac0>a&9h!4Ng~ zR~*l8n?!UnXT6wDP3;zYb}~kek?rX4{|g|U0;0yMSy?Krq1O*N%*RO;J#a{@VD#2w zk;1t?NI^M$z*kutUoWEZGNqaE1%HqJiA74g$`YXaP=zF!^nnN-R~RM?`vyb1)(Uj! z9v<$YBK@!L4Wq&9@YzRbV{Mq)dilxJvw8P=LZ9!=Gn#qds1$zs>t?(Ay<(Q*-7fqY zA4Aev%(Wl&=OOBc6{VJAd;RQdGrHQM>HC%GSr6(WL6CBFq#yaCb!nh|0}KOuBJDX) zqE??w;rAm7^n9drMX1uEk||GPR2G})_H#`y6}$@bFdSOM;*XE$!vkVAm)e{(*(kqB zlC!y3`kvAV;%KWMh_lu5#~zu6*`U>Dcq&1B0RiCV%_w()<@iVzt9S;3Kggaz6lsm7 z!E6omj2|^bW}?jZSWmi?d8+`q3=w6B)5K}RbCrd-U{6&YnlGB9cqbGp1*&rTH{Q$s z$xgFNB7dca#HCu1fO9j0SP)YF1hY}AGBDCU$+4h0h9p46WO_ifKv`Kh;)eYXYKQA5 z&Uv!o?y<*zae%{Te|dG8rSRsobxsyOd6Dz!>Wrzx4_f|gy^*nsgPFy_aGjHL+_Nz{ zB6FbyMf!D&k}$;$SEheTThs{^3O=b5C@dN_2@RjHg|J)OmkLmNnsT9q-TYP zxC>#AZ924qPfmgBhMjOCM)@voS9UNNi<7H~8B(@Kv9OKNW2hXb1hK;771Nuhh4UIY z1ekwK9FP!4mpP858-hl)dp-EJGtA)*yTo8VNS_o~+)^{bsNf*0VyaKabhcSY7-Woe zQ{M;5qd@=kJXONG7!MO*T&N@CRdl*rQ{Akf-DT@V+PW@T$%;Cwa{dITJ!N^xH?&5X z2AP1bu{er4o02CEa{5itQGo^yPAIciq%#tv5BUR+QYfGJ^*uuZ8R^q2N1KMcgiR15?x{kj#Lvb50AwVUas3ph=^4$Q1Y=R2+=;mc7N z`>GA5si;w3>C>_q$hiqZDcB z3Ty=F#3m|aP=i|I8}K~t2dW}>^7)AurDyK5-xu(S)}`APOP-3}jY?d&#l=*55pG?0-w{jQafGnu9mcvbnWo6Aov39@AEF7*G9JHDN4WjS9J!G&^ zzm1uy>*p5DsQi+uz>CczeVJZn)W`wjmRug)zRT*4rHBpn;7%*AmZe4AOAD`T;gaL5 zaQo^3O46+40p02GH*nKjCOwci;plc+{|6Xi9Aq0A21n^M=`5Kl!wy98Cc>=uh*~$)`+huE?5*0@iXBU_1m@R7$yLi~bEPNpwn_r-Gdj zBSlQoSrU72gJ5D@C4yiq1{XNCDN;ZGsSDO3smGT`hwf-DE{yX3()ieTI90ro*^a&V zt5wPDX0PnaU2At;T#4l?Axpp$hVfH&0K#2!cm7`@E8czkRv$Z;4`FoL?`aKGm}40& z#G(zst!+%@^>Kt04PEO;7>XOJK^|W4zS&L<`1>heJj+6(0eIM1O}rN7d|LrKU(IA1 z!2M}RF?k;s=nUVy;b)n~y^Y!chUYEc1}v4PgVb!N=6`&~=v+-qm~fSEIZ^ySW9Y>MFh%dW8%DN06d@}lh2RJPNYQSaOJ_`dH_0U!(fDMdeY zIt8|<5`7u$enLn!fh`Jxoov_By&(ogAW0z&bs&_~?)LG>O%#7-?2q>amztlM%3W*9XRGbTTYY{DPKT3- zBce=QZ!#+ep7*A2fH>hxF;th;Cd;m!!V&ERV0U~`i>4wC)JdEX)6c~TJ|3fagat|h zbqPETXNPbbv^7A0^pU$t5W!;7IGsq@)gnv>9`-;o1R26_ry|X?h+MB%@01Iq za+lZVmcy+em=_vt*K8jy`#uZDGmTH7zAiIxcxHsk;o{;l)u$P?Vq51DPZ4Zt zYS!NQs28J1Nu@c)m)N5=Tk*kbcb@g(m8pXbh(gEP=kJZXFf|+W4D;Pa5un^?(c7QISYij6IN}Jxvp+A zFj(yfZvMU36)a4vyiR>q5t*(Rg&&!n`*1^^0=*R_g~q35VC$|{6kT|Qddr`=EsgFB z&3`!bE~Zb-8tBV(mv=SOm>$GR{HP?JtPH6+Llvg`@DMmg=QKAccpOB#kGaQZ3=SX~ zNitALbN3c|s`&yJ>yE|82O%#C4-<5oNOH~baFZ{XIsSeVX^U9(y`R&18^m#YOOJ=x zq7S)3Jb+{_s!<+(ou%YFg0zZ#RHqm8OcirZSuVcuFo+elpSlw}XOQ5rf<~!s5hWPc`H4To%W?JsM|1F1);f7II2rdS= z*&^sgQVl{fcQEx6RUS2okA$an)##9T-6Pr~%5jJWg!5nzj5dRU0}(;W+*&6`&&_;w zh?l#7HZ&$23&Q%6EM8~OYTeH_Vm;7WsHWMEel5YVV-MDgUeIw6$!b_Vvs-@9@07Mx5wv z1ku+#j4htx<(*}lWN9bdlP!2`D9YsLyzkGc(rNvAX@$|@Gn~8e^PMuUtDPs0PLX3g zYh-+rJ+sCunx#T4efT9!Fn!s~yZj~KG%uYYxMLf|c>5qd%t%<#_1w@vXGVJHQn7+y zxWKVUs=5D%00SM-M4SR?d>yvR1%>PQ2k7NMA^P-#V$%+4f*rH?kW779(z%}PQ&skl zK&NZ`0rvb$6r0TOtYYdtWu{1SiKLcbChjDb1=&v#eHDINCM8OeT9V1#Ik5SN3}BCX z&zAJ7A0MBj3|6ZEeiyQ;{4Ql&CxS=VPE0b~UfX$SdXw)O&F#Gq9(9^-g-h(!K?m1b3>MU0kCKnAQ-7h5%e`3k-E2({IUz)ih5 zNNi|Qo}QN+C%+-*FNR);6#V8x6f7J0ja#I>?@}@#rh(4gL_tzX(30K1T1nSWsrMCe zeL04Cxa17*`42>eshXl6&_)XZAOygpU?~&Gu28+c^A84}XZBW3FOdjLFgs=R6&Lvx zhJty1z2R+au&0Vuu0xH1!c3Q7%_+(+zUEwpy+(RsK(|K_8O@j+m7K{A2J}X%0&fpu zA}>lP-Qqd?wwIdmmQ73vdjvq++V(OxTsOJ9SHd

Hp=&b zG+tTHy?q;zseO^`3G%wv%Ac-1-~xm`|6!T)ITjJbY4PJdBUYFSmY(V#E*x66EmBG| z5oy8OB67nOhugg)iwAT46=(~E4+f$-I&i+0nn<7QfjjOBuBGz$<>?(c|6bG3cf||t zr|KmV2bE@n=aSqL2aaW_s=~3oQ@7)_;#gaG4I79smmTG z!DB?g=a;uN6f7%@^OjT1y+VMx4eqnn8!??Gv21!0a{HMGdWekZq;lnndwlL4X$3y_ z%4o;)%#9<`qV{4vd!;&YhO-ID@|6)smm1k%&zsQmV#0@KR5a^YpGccWhR(*w10h*H z$-j)yc!}pbM|clAaH101wYYH=mIq;?Pcx9k+=wf?m^GD7c}Fn*r0UZphk2Jt_|Y5~ zZ~n-!2d{iY#YRbMvI%MZQNfZ9s($kM?SYEE>7b33P5av`+eP`!0W4)NXwQ??1^*K% z=^Jt-XzQ@%dqDDaW`AkIZcV!$*2Msc$#~K`#m$zpsrO3Fn5od}ve%+wosS1Mg4q&U$0N4>$>P;41tR z0*VunpYH_e7o5?*tV@Qq3`npHM#!X(9yqzG?<{J}|KW6Zrs+=nTNQTm7ew8({U5UT znQ%c&QDs@DQ0?kVtC5=!S?Hy8582GSAnHW1GOvIS=DyQ^zMUg_y?&a!c<^h<=e5@Q z*2!I#Q;jI9Bbh0=Om@F6Ph4vFtZ`6s;6KPqfWH~OTxMG7X*pFtPG5F3i$2QozjSrs z57ts!F5RaBceztM`DTIpm_E8Lf}Z4`)bc?FkCvxT4!o4ld9atXw? z5-Bn%3Mncn8Ywy{1}R%AX8wzM_UyE8XGSiDW){av`TssnI@BM$T`y1Zbl;xV@CXO_ z8wg#qp9R&0b41ida75Nca@eHB%S*g7zK<;am-zh>u%zjR&zU@d`Tny4fQ*Eqc(tf; G@c#j<^SvAZ diff --git a/icon.psd b/icon.psd index 8ba189d22117817639ca003e8b034b8f66e862b4..f8cec3513c619b436bef7b5bf619c1a29d1b99ce 100644 GIT binary patch delta 23527 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�LQt(Ms@*a!J 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 delta 29231 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 Date: Fri, 14 Jun 2024 15:53:13 +0800 Subject: [PATCH 037/108] =?UTF-8?q?EnsureMatchAcceptContentType=E7=9A=84?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E4=B8=BAfalse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Attributes/ReturnAttributes/ApiReturnAttribute.cs | 4 ++-- .../Implementations/DefaultApiReturnDescriptor.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs index 1f516c60..e38fa3b5 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs @@ -51,9 +51,9 @@ public virtual int OrderIndex ///

/// 获取或设置是否确保响应的ContentType与指定的Accpet-ContentType一致 - /// 默认为true + /// 默认为false /// - public bool EnsureMatchAcceptContentType { get; set; } = true; + public bool EnsureMatchAcceptContentType { get; set; } = false; /// /// 响应内容处理的抽象特性 diff --git a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs index bfc03112..5e5b7afe 100644 --- a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs @@ -84,7 +84,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 +92,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 }; } } From 17436b0533680ce50364a687831c8ab75a166b05 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 14 Jun 2024 18:40:26 +0800 Subject: [PATCH 038/108] =?UTF-8?q?=E6=89=8B=E5=8A=A8=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE=E4=BB=A5=E9=80=82=E9=85=8DAOT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpRequestHeaderExtensions.cs | 118 ++++++++++++++---- WebApiClientCore/HttpRequestHeader.cs | 6 +- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs index d0939236..d5d57dc0 100644 --- a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; namespace WebApiClientCore { @@ -11,26 +7,101 @@ namespace WebApiClientCore /// static class HttpRequestHeaderExtensions { - /// - /// HttpRequestHeader的类型 - /// - private static readonly Type httpRequestHeaderType = typeof(HttpRequestHeader); - /// /// 请求头枚举和名称的缓存 /// - private static readonly Dictionary cache = []; + private static readonly Dictionary cache = new() + { + [HttpRequestHeader.CacheControl] = nameof(HttpRequestHeader.CacheControl), + [HttpRequestHeader.Connection] = nameof(HttpRequestHeader.Connection), + [HttpRequestHeader.Date] = nameof(HttpRequestHeader.Date), + [HttpRequestHeader.KeepAlive] = nameof(HttpRequestHeader.KeepAlive), + [HttpRequestHeader.Pragma] = nameof(HttpRequestHeader.Pragma), + [HttpRequestHeader.Trailer] = nameof(HttpRequestHeader.Trailer), + [HttpRequestHeader.TransferEncoding] = nameof(HttpRequestHeader.TransferEncoding), + [HttpRequestHeader.Upgrade] = nameof(HttpRequestHeader.Upgrade), + [HttpRequestHeader.Via] = nameof(HttpRequestHeader.Via), + [HttpRequestHeader.Warning] = nameof(HttpRequestHeader.Warning), + [HttpRequestHeader.Allow] = nameof(HttpRequestHeader.Allow), + [HttpRequestHeader.ContentLength] = nameof(HttpRequestHeader.ContentLength), + [HttpRequestHeader.ContentType] = nameof(HttpRequestHeader.ContentType), + [HttpRequestHeader.ContentEncoding] = nameof(HttpRequestHeader.ContentEncoding), + [HttpRequestHeader.ContentLanguage] = nameof(HttpRequestHeader.ContentLanguage), + [HttpRequestHeader.ContentLocation] = nameof(HttpRequestHeader.ContentLocation), + [HttpRequestHeader.ContentMd5] = nameof(HttpRequestHeader.ContentMd5), + [HttpRequestHeader.ContentRange] = nameof(HttpRequestHeader.ContentRange), + [HttpRequestHeader.Expires] = nameof(HttpRequestHeader.Expires), + [HttpRequestHeader.LastModified] = nameof(HttpRequestHeader.LastModified), + [HttpRequestHeader.Accept] = nameof(HttpRequestHeader.Accept), + [HttpRequestHeader.AcceptCharset] = nameof(HttpRequestHeader.AcceptCharset), + [HttpRequestHeader.AcceptEncoding] = nameof(HttpRequestHeader.AcceptEncoding), + [HttpRequestHeader.AcceptLanguage] = nameof(HttpRequestHeader.AcceptLanguage), + [HttpRequestHeader.Authorization] = nameof(HttpRequestHeader.Authorization), + [HttpRequestHeader.Cookie] = nameof(HttpRequestHeader.Cookie), + [HttpRequestHeader.Expect] = nameof(HttpRequestHeader.Expect), + [HttpRequestHeader.From] = nameof(HttpRequestHeader.From), + [HttpRequestHeader.Host] = nameof(HttpRequestHeader.Host), + [HttpRequestHeader.IfMatch] = nameof(HttpRequestHeader.IfMatch), + [HttpRequestHeader.IfModifiedSince] = nameof(HttpRequestHeader.IfModifiedSince), + [HttpRequestHeader.IfNoneMatch] = nameof(HttpRequestHeader.IfNoneMatch), + [HttpRequestHeader.IfRange] = nameof(HttpRequestHeader.IfRange), + [HttpRequestHeader.IfUnmodifiedSince] = nameof(HttpRequestHeader.IfUnmodifiedSince), + [HttpRequestHeader.MaxForwards] = nameof(HttpRequestHeader.MaxForwards), + [HttpRequestHeader.ProxyAuthorization] = nameof(HttpRequestHeader.ProxyAuthorization), + [HttpRequestHeader.Referer] = nameof(HttpRequestHeader.Referer), + [HttpRequestHeader.Range] = nameof(HttpRequestHeader.Range), + [HttpRequestHeader.Te] = nameof(HttpRequestHeader.Te), + [HttpRequestHeader.Translate] = nameof(HttpRequestHeader.Translate), + [HttpRequestHeader.UserAgent] = nameof(HttpRequestHeader.UserAgent) + }; /// - /// 请求头枚举到名称的转换 + /// 请求头枚举和名称的缓存 /// - static HttpRequestHeaderExtensions() + private static readonly Dictionary displayCache = new() { - foreach (var header in Enum.GetValues(httpRequestHeaderType).Cast()) - { - cache.Add(header, header.GetHeaderName()); - } - } + [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" + }; /// /// 返回枚举的DisplayName @@ -39,10 +110,7 @@ static HttpRequestHeaderExtensions() /// private static string GetHeaderName(this HttpRequestHeader header) { - return httpRequestHeaderType - .GetField(header.ToString())? - .GetCustomAttribute()? - .Name ?? header.ToString(); + return displayCache.TryGetValue(header, out var name) ? name : header.ToString(); } /// @@ -52,11 +120,7 @@ private static string GetHeaderName(this HttpRequestHeader 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 : header.ToString(); } } -} \ No newline at end of file +} 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, From 97ed1c4fc968cf57e328ecd0e2434b330664b59b Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 14 Jun 2024 19:14:41 +0800 Subject: [PATCH 039/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpRequestHeaderExtensions.cs | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs index d5d57dc0..daaaaebc 100644 --- a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs @@ -11,54 +11,6 @@ static class HttpRequestHeaderExtensions /// 请求头枚举和名称的缓存 /// private static readonly Dictionary cache = new() - { - [HttpRequestHeader.CacheControl] = nameof(HttpRequestHeader.CacheControl), - [HttpRequestHeader.Connection] = nameof(HttpRequestHeader.Connection), - [HttpRequestHeader.Date] = nameof(HttpRequestHeader.Date), - [HttpRequestHeader.KeepAlive] = nameof(HttpRequestHeader.KeepAlive), - [HttpRequestHeader.Pragma] = nameof(HttpRequestHeader.Pragma), - [HttpRequestHeader.Trailer] = nameof(HttpRequestHeader.Trailer), - [HttpRequestHeader.TransferEncoding] = nameof(HttpRequestHeader.TransferEncoding), - [HttpRequestHeader.Upgrade] = nameof(HttpRequestHeader.Upgrade), - [HttpRequestHeader.Via] = nameof(HttpRequestHeader.Via), - [HttpRequestHeader.Warning] = nameof(HttpRequestHeader.Warning), - [HttpRequestHeader.Allow] = nameof(HttpRequestHeader.Allow), - [HttpRequestHeader.ContentLength] = nameof(HttpRequestHeader.ContentLength), - [HttpRequestHeader.ContentType] = nameof(HttpRequestHeader.ContentType), - [HttpRequestHeader.ContentEncoding] = nameof(HttpRequestHeader.ContentEncoding), - [HttpRequestHeader.ContentLanguage] = nameof(HttpRequestHeader.ContentLanguage), - [HttpRequestHeader.ContentLocation] = nameof(HttpRequestHeader.ContentLocation), - [HttpRequestHeader.ContentMd5] = nameof(HttpRequestHeader.ContentMd5), - [HttpRequestHeader.ContentRange] = nameof(HttpRequestHeader.ContentRange), - [HttpRequestHeader.Expires] = nameof(HttpRequestHeader.Expires), - [HttpRequestHeader.LastModified] = nameof(HttpRequestHeader.LastModified), - [HttpRequestHeader.Accept] = nameof(HttpRequestHeader.Accept), - [HttpRequestHeader.AcceptCharset] = nameof(HttpRequestHeader.AcceptCharset), - [HttpRequestHeader.AcceptEncoding] = nameof(HttpRequestHeader.AcceptEncoding), - [HttpRequestHeader.AcceptLanguage] = nameof(HttpRequestHeader.AcceptLanguage), - [HttpRequestHeader.Authorization] = nameof(HttpRequestHeader.Authorization), - [HttpRequestHeader.Cookie] = nameof(HttpRequestHeader.Cookie), - [HttpRequestHeader.Expect] = nameof(HttpRequestHeader.Expect), - [HttpRequestHeader.From] = nameof(HttpRequestHeader.From), - [HttpRequestHeader.Host] = nameof(HttpRequestHeader.Host), - [HttpRequestHeader.IfMatch] = nameof(HttpRequestHeader.IfMatch), - [HttpRequestHeader.IfModifiedSince] = nameof(HttpRequestHeader.IfModifiedSince), - [HttpRequestHeader.IfNoneMatch] = nameof(HttpRequestHeader.IfNoneMatch), - [HttpRequestHeader.IfRange] = nameof(HttpRequestHeader.IfRange), - [HttpRequestHeader.IfUnmodifiedSince] = nameof(HttpRequestHeader.IfUnmodifiedSince), - [HttpRequestHeader.MaxForwards] = nameof(HttpRequestHeader.MaxForwards), - [HttpRequestHeader.ProxyAuthorization] = nameof(HttpRequestHeader.ProxyAuthorization), - [HttpRequestHeader.Referer] = nameof(HttpRequestHeader.Referer), - [HttpRequestHeader.Range] = nameof(HttpRequestHeader.Range), - [HttpRequestHeader.Te] = nameof(HttpRequestHeader.Te), - [HttpRequestHeader.Translate] = nameof(HttpRequestHeader.Translate), - [HttpRequestHeader.UserAgent] = nameof(HttpRequestHeader.UserAgent) - }; - - /// - /// 请求头枚举和名称的缓存 - /// - private static readonly Dictionary displayCache = new() { [HttpRequestHeader.CacheControl] = "Cache-Control", [HttpRequestHeader.Connection] = "Connection", @@ -110,7 +62,7 @@ static class HttpRequestHeaderExtensions /// private static string GetHeaderName(this HttpRequestHeader header) { - return displayCache.TryGetValue(header, out var name) ? name : header.ToString(); + return cache.TryGetValue(header, out var name) ? name : header.ToString(); } /// From 9a01a6afc367a585390cc002273a3b5eab055c0e Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 14 Jun 2024 19:23:18 +0800 Subject: [PATCH 040/108] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=A7=81=E6=9C=89?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuildinExtensions/HttpRequestHeaderExtensions.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs index daaaaebc..9c68b6d0 100644 --- a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs @@ -55,16 +55,6 @@ static class HttpRequestHeaderExtensions [HttpRequestHeader.UserAgent] = "User-Agent" }; - /// - /// 返回枚举的DisplayName - /// - /// 请求头枚举 - /// - private static string GetHeaderName(this HttpRequestHeader header) - { - return cache.TryGetValue(header, out var name) ? name : header.ToString(); - } - /// /// 转换为header名 /// From 62f50e472e947fea45a536e6c02014311ec1847d Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 14 Jun 2024 21:18:49 +0800 Subject: [PATCH 041/108] =?UTF-8?q?=E5=AE=8C=E5=96=84HttpRequestHeader?= =?UTF-8?q?=E7=9A=84=E8=8C=83=E5=9B=B4=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpRequestHeaderExtensionsTest.cs | 13 ++++++++++++- .../Attributes/ActionAttributes/HeaderAttribute.cs | 1 + .../ParameterAttributes/HeaderAttribute.cs | 1 + .../HttpRequestHeaderExtensions.cs | 4 +++- 4 files changed, 17 insertions(+), 2 deletions(-) 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/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/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/BuildinExtensions/HttpRequestHeaderExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs index 9c68b6d0..f4773471 100644 --- a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace WebApiClientCore @@ -59,10 +60,11 @@ static class HttpRequestHeaderExtensions /// 转换为header名 ///
/// 请求头枚举 + /// /// public static string ToHeaderName(this HttpRequestHeader header) { - return cache.TryGetValue(header, out var name) ? name : header.ToString(); + return cache.TryGetValue(header, out var name) ? name : throw new ArgumentOutOfRangeException(nameof(header)); } } } From 5ff831f57645fde3453d3622bbb232d1d2f16c80 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 14 Jun 2024 21:56:50 +0800 Subject: [PATCH 042/108] 2.1.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index b2ecb7a3..a059d206 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.1.1 + 2.1.2 Copyright © laojiu 2017-2024 IDE0057;IDE0290 From b1f4e77c0a07f21b8964ffc7619d0d797120b4b4 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 14 Jun 2024 22:16:34 +0800 Subject: [PATCH 043/108] add test --- .../JsonCompatibleConverterTest.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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); + } } } From 0997c92d69d7a0b3bfab6cb60a948821725b5106 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 14 Jun 2024 22:42:48 +0800 Subject: [PATCH 044/108] =?UTF-8?q?=E7=AE=80=E5=8C=96demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/{Clients => ApiClients}/IUserApi.cs | 20 +++---- .../UserHostedService.cs | 18 +++---- App/{Clients => ApiClients}/UserService.cs | 5 +- App/App.csproj | 3 +- App/Attributes/ServiceNameAttribute.cs | 41 -------------- App/Clients/DynamicHostDemoHostedService.cs | 35 ------------ App/Clients/DynamicHostDemoService.cs | 22 -------- App/Clients/IDynamicHostDemo.cs | 24 --------- App/Clients/IUserApi_ParameterStyle.cs | 54 ------------------- App/Controllers/TokenFilterAttribute.cs | 2 - App/Controllers/UsersController.cs | 9 ++-- App/Extensions/DIExtensions4DynamicHost.cs | 26 --------- App/Filters/UriFilterAttribute.cs | 37 ------------- App/Models/Gender.cs | 11 ++++ App/{ => Models}/User.cs | 19 ++----- App/README.md | 9 ++-- App/Services/HostProvider.cs | 35 ------------ App/Startup.cs | 5 +- 18 files changed, 48 insertions(+), 327 deletions(-) rename App/{Clients => ApiClients}/IUserApi.cs (81%) rename App/{Clients => ApiClients}/UserHostedService.cs (52%) rename App/{Clients => ApiClients}/UserService.cs (95%) delete mode 100644 App/Attributes/ServiceNameAttribute.cs delete mode 100644 App/Clients/DynamicHostDemoHostedService.cs delete mode 100644 App/Clients/DynamicHostDemoService.cs delete mode 100644 App/Clients/IDynamicHostDemo.cs delete mode 100644 App/Clients/IUserApi_ParameterStyle.cs delete mode 100644 App/Extensions/DIExtensions4DynamicHost.cs delete mode 100644 App/Filters/UriFilterAttribute.cs create mode 100644 App/Models/Gender.cs rename App/{ => Models}/User.cs (63%) delete mode 100644 App/Services/HostProvider.cs diff --git a/App/Clients/IUserApi.cs b/App/ApiClients/IUserApi.cs similarity index 81% rename from App/Clients/IUserApi.cs rename to App/ApiClients/IUserApi.cs index 972e516f..460ac8bb 100644 --- a/App/Clients/IUserApi.cs +++ b/App/ApiClients/IUserApi.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using App.Models; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Net.Http; using System.Threading; @@ -12,8 +13,8 @@ namespace App.Clients /// /// 用户操作接口 /// - [LoggingFilter] [OAuthToken] + [LoggingFilter] public interface IUserApi : IHttpApi { [HttpGet("api/users/{account}")] @@ -21,17 +22,14 @@ public interface IUserApi : IHttpApi [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}")] + 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 GetExpectXmlAsync([Required] string account, CancellationToken token = default); [HttpGet("api/users/{account}")] @@ -58,8 +56,6 @@ public interface IUserApi : IHttpApi [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/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 95% rename from App/Clients/UserService.cs rename to App/ApiClients/UserService.cs index bb0b297b..1ced13e7 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 { diff --git a/App/App.csproj b/App/App.csproj index 207f2462..7bba3f9b 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -2,7 +2,8 @@ Exe - net6.0 + enable + net6.0 false false 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_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/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..55ba3c0b 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; @@ -47,8 +46,7 @@ public void ConfigureServices(IServiceCollection services) // ȫĬ services - .AddWebApiClient() - .ConfigureHttpApi(o => { }) + .AddWebApiClient() .UseJsonFirstApiActionDescriptor(); // עuserApi @@ -68,7 +66,6 @@ public void ConfigureServices(IServiceCollection services) // userApiͻ˺̨ services.AddScoped().AddHostedService(); - services.AddDynamicHostSupport(); } /// From 58e4edffb255c3b4bc9eadf59afe864ecee133cf Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 14 Jun 2024 22:48:50 +0800 Subject: [PATCH 045/108] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E4=BF=AE=E8=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/ApiClients/IUserApi.cs | 41 +++++++++++----------- App/ApiClients/UserService.cs | 8 ++--- "App/\346\226\207\344\273\266TextFile.txt" | 1 - 3 files changed, 23 insertions(+), 27 deletions(-) delete mode 100644 "App/\346\226\207\344\273\266TextFile.txt" diff --git a/App/ApiClients/IUserApi.cs b/App/ApiClients/IUserApi.cs index 460ac8bb..46b2ec6d 100644 --- a/App/ApiClients/IUserApi.cs +++ b/App/ApiClients/IUserApi.cs @@ -1,5 +1,4 @@ using App.Models; -using System.ComponentModel.DataAnnotations; using System.IO; using System.Net.Http; using System.Threading; @@ -14,49 +13,49 @@ namespace App.Clients /// 用户操作接口 /// [OAuthToken] - [LoggingFilter] + [LoggingFilter] public interface IUserApi : IHttpApi { [HttpGet("api/users/{account}")] - ITask GetAsync([Required] string account); + ITask GetAsync(string account); [HttpGet("api/users/{account}")] - ITask GetAsStringAsync([Required] string account, CancellationToken token = default); - - [JsonReturn] - [HttpGet("api/users/{account}")] - ITask GetExpectJsonAsync([Required] string account, CancellationToken token = default); + ITask GetAsStringAsync(string account, CancellationToken token = default); - [XmlReturn] - [HttpGet("api/users/{account}")] - ITask GetExpectXmlAsync([Required] 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 GetAsByteArrayAsync([Required] string account, CancellationToken token = default); + ITask GetAsModelAsync(string account, CancellationToken token = default); + [JsonReturn] [HttpGet("api/users/{account}")] - ITask GetAsStreamAsync([Required] string account, CancellationToken token = default); + ITask GetExpectJsonAsync(string account, CancellationToken token = default); + [XmlReturn] [HttpGet("api/users/{account}")] - ITask GetAsModelAsync([Required] string account, CancellationToken token = default); + ITask GetExpectXmlAsync(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); - + 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([Required, FormContent] User user, CancellationToken token = default); + Task PostByFormAsync([FormContent] User user, CancellationToken token = default); [HttpPost("api/users/formdata")] - Task PostByFormDataAsync([Required, FormDataContent] User user, FormDataFile file, CancellationToken token = default); + Task PostByFormDataAsync([FormDataContent] User user, FormDataFile file, CancellationToken token = default); + + [HttpDelete("api/users/{account}")] - Task DeleteAsync([Required] string account); + Task DeleteAsync(string account); } } diff --git a/App/ApiClients/UserService.cs b/App/ApiClients/UserService.cs index 1ced13e7..0e77c99d 100644 --- a/App/ApiClients/UserService.cs +++ b/App/ApiClients/UserService.cs @@ -27,17 +27,15 @@ public async Task RunRequestsAsync() }; // 上传的文件 - 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/\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 From 852ab28c195a94b3f475dcd852257b6ec64f846c Mon Sep 17 00:00:00 2001 From: Ezreal Date: Sat, 15 Jun 2024 09:48:18 +0800 Subject: [PATCH 046/108] sync docs md files --- docs/Readme.md | 30 - docs/guide/1_getting-started.md | 135 ++++ docs/guide/2_attributes.md | 451 +++++++++++++ docs/guide/3_special-type.md | 75 +++ docs/guide/4_data-validation.md | 54 ++ docs/guide/5_advanced.md | 626 ++++++++++++++++++ docs/guide/6_auth-token-extension.md | 205 ++++++ docs/guide/7_json-net-extension.md | 29 + .../{json-rpc.md => 8_jsonrpc-extension.md} | 11 +- docs/guide/9_openapi-to-code.md | 32 + docs/guide/attribute.md | 65 -- docs/guide/config.md | 51 -- docs/guide/core-analyzers.md | 6 - docs/guide/data-validation.md | 34 - docs/guide/deformed-interface.md | 132 ---- docs/guide/diy-request-response.md | 59 -- docs/guide/dynamic-host.md | 117 ---- docs/guide/exception-process.md | 37 -- docs/guide/file-download.md | 17 - docs/guide/getting-started.md | 153 ----- docs/guide/http-message-handler.md | 117 ---- docs/guide/interface-demo.md | 91 --- docs/guide/jsonnet.md | 36 - docs/guide/log.md | 39 -- docs/guide/oauths-token.md | 200 ------ docs/guide/readme.md | 22 +- docs/guide/request.md | 179 ----- docs/guide/response.md | 89 --- docs/guide/retry.md | 16 - docs/guide/source-generator.md | 13 - docs/guide/url-rule.md | 18 - docs/old/advanced/env-with-di.md | 6 +- docs/old/advanced/env-without-di.md | 4 +- docs/old/basic/attribute-scope-features.md | 6 +- docs/old/basic/full-demo.md | 6 +- docs/old/basic/get-head.md | 14 +- docs/old/basic/parameter-attribute.md | 6 +- docs/old/basic/parameter-validation.md | 8 +- docs/old/basic/patch.md | 6 +- docs/old/basic/post-put-delete.md | 10 +- docs/old/basic/request-url.md | 20 +- docs/old/getting-started.md | 4 +- docs/old/qa.md | 18 +- docs/reference/nuget.md | 10 - 44 files changed, 1675 insertions(+), 1582 deletions(-) delete mode 100644 docs/Readme.md create mode 100644 docs/guide/1_getting-started.md create mode 100644 docs/guide/2_attributes.md create mode 100644 docs/guide/3_special-type.md create mode 100644 docs/guide/4_data-validation.md create mode 100644 docs/guide/5_advanced.md create mode 100644 docs/guide/6_auth-token-extension.md create mode 100644 docs/guide/7_json-net-extension.md rename docs/guide/{json-rpc.md => 8_jsonrpc-extension.md} (56%) create mode 100644 docs/guide/9_openapi-to-code.md delete mode 100644 docs/guide/attribute.md delete mode 100644 docs/guide/config.md delete mode 100644 docs/guide/core-analyzers.md delete mode 100644 docs/guide/data-validation.md delete mode 100644 docs/guide/deformed-interface.md delete mode 100644 docs/guide/diy-request-response.md delete mode 100644 docs/guide/dynamic-host.md delete mode 100644 docs/guide/exception-process.md delete mode 100644 docs/guide/file-download.md delete mode 100644 docs/guide/getting-started.md delete mode 100644 docs/guide/http-message-handler.md delete mode 100644 docs/guide/interface-demo.md delete mode 100644 docs/guide/jsonnet.md delete mode 100644 docs/guide/log.md delete mode 100644 docs/guide/oauths-token.md delete mode 100644 docs/guide/request.md delete mode 100644 docs/guide/response.md delete mode 100644 docs/guide/retry.md delete mode 100644 docs/guide/source-generator.md delete mode 100644 docs/guide/url-rule.md delete mode 100644 docs/reference/nuget.md 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) | From a0d89ce1f4e0039aa35c277fca75bd12a04f2031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Sat, 15 Jun 2024 14:36:06 +0800 Subject: [PATCH 047/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Aot=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiActionDescriptor.cs | 4 ++ .../IApiActionDescriptorProvider.cs | 7 +- .../IHttpApiActivator.cs | 6 +- .../KeyValueSerializerOptions.cs | 10 +++ .../WebApiClientCore.Abstractions.csproj | 3 +- .../TokenHandlerExtensions.cs | 4 ++ ...TokenProviderExtensions.GrantTypeClient.cs | 19 ++++- ...kenProviderExtensions.GrantTypePassword.cs | 19 ++++- .../TokenProviderExtensions.cs | 25 +++++-- .../ITokenProviderFactory.cs | 13 +++- .../TokenProviderFactory.cs | 28 ++++++-- .../ClientCredentialsTokenProvider.cs | 9 +++ .../TokenProviders/OAuth2TokenClient.cs | 17 +++++ .../PasswordCredentialsTokenProvider.cs | 9 +++ .../WebApiClientCore.Extensions.OAuths.csproj | 5 +- .../HttpRequestHeaderExtensionsTest.cs | 2 + WebApiClientCore.Test/Internals/LambdaTest.cs | 16 +---- .../Serialization/KeyValueSerializerTest.cs | 19 ++++- .../ApiParameterContextExtensions.cs | 20 ++++++ .../ApiResponseContextExtensions.cs | 8 +++ .../FormContentAttribute.cs | 9 +++ .../FormDataContentAttribute.cs | 5 ++ .../ParameterAttributes/HeadersAttribute.cs | 7 +- .../JsonContentAttribute.cs | 5 ++ .../JsonFormDataTextAttribute.cs | 7 +- .../JsonFormFieldAttribute.cs | 7 +- .../ParameterAttributes/PathQueryAttribute.cs | 9 +++ .../XmlContentAttribute.cs | 4 ++ .../ReturnAttributes/JsonReturnAttribute.cs | 9 ++- .../ReturnAttributes/XmlReturnAttribute.cs | 8 ++- .../BuildinExtensions/TypeExtensions.cs | 13 +++- .../DependencyInjection/HttpApiExtensions.cs | 39 ++++++++--- WebApiClientCore/HttpApi.cs | 10 ++- WebApiClientCore/HttpContents/FormContent.cs | 5 ++ WebApiClientCore/HttpContents/JsonContent.cs | 11 ++- .../HttpContents/JsonPatchContent.cs | 5 ++ .../Implementations/DataValidator.cs | 10 +++ .../DefaultApiActionDescriptor.cs | 13 +++- .../DefaultApiActionDescriptorProvider.cs | 7 +- .../DefaultApiActionInvokerProvider.cs | 6 +- .../DefaultApiReturnDescriptor.cs | 10 ++- .../DefaultHttpApiActivator.cs | 6 +- .../Implementations/HttpApiActivator.cs | 7 +- .../Implementations/ILEmitHttpApiActivator.cs | 20 ++++-- .../JsonFirstApiActionDescriptor.cs | 7 +- .../JsonFirstApiActionDescriptorProvider.cs | 7 +- .../SourceGeneratorHttpApiActivator.cs | 16 ++++- .../SourceGeneratorProxyClassFinder.cs | 6 +- WebApiClientCore/Internals/LambdaUtil.cs | 69 +++++++++---------- .../Parameters/JsonPatchDocument.cs | 5 ++ .../Serialization/JsonBufferSerializer.cs | 8 ++- .../JsonConverters/JsonStringTypeConverter.cs | 13 +++- .../Serialization/KeyValueSerializer.cs | 9 +++ .../Serialization/XmlSerializer.cs | 7 ++ WebApiClientCore/WebApiClientCore.csproj | 7 +- 55 files changed, 506 insertions(+), 123 deletions(-) diff --git a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs index ea08740e..cd981970 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 @@ -14,6 +15,9 @@ public abstract class ApiActionDescriptor /// 获取所在接口类型 /// 这个值不一定是声明方法的接口类型 /// +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif public abstract Type InterfaceType { get; protected set; } /// diff --git a/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs b/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs index d6124b87..c6748e81 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,10 @@ public interface IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - ApiActionDescriptor CreateActionDescriptor(MethodInfo method, Type interfaceType); + ApiActionDescriptor CreateActionDescriptor(MethodInfo method, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type interfaceType); } } diff --git a/WebApiClientCore.Abstractions/IHttpApiActivator.cs b/WebApiClientCore.Abstractions/IHttpApiActivator.cs index 46137951..1d6ec50f 100644 --- a/WebApiClientCore.Abstractions/IHttpApiActivator.cs +++ b/WebApiClientCore.Abstractions/IHttpApiActivator.cs @@ -1,4 +1,6 @@ -namespace WebApiClientCore +using System.Diagnostics.CodeAnalysis; + +namespace WebApiClientCore { /// /// 定义THttpApi的实例创建器的接口 @@ -6,7 +8,7 @@ /// public interface IHttpApiActivator< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi> { diff --git a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs index 0034bbbc..cc63c3e7 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; @@ -40,8 +41,13 @@ public JsonNamingPolicy? DictionaryKeyPolicy /// 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,10 @@ public KeyValueSerializerOptions() /// /// 目标类型 /// +#if NET8_0_OR_GREATER + [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.")] +#endif 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 abca34f8..f36dc8e1 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -1,8 +1,9 @@ - enable + enable netstandard2.1;net5.0;net8.0 + true WebApiClientCore WebApiClientCore.Abstractions diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs index 4a2838bd..813afac5 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using WebApiClientCore.Extensions.OAuths; using WebApiClientCore.Extensions.OAuths.HttpMessageHandlers; @@ -38,6 +39,9 @@ public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder bu /// hanlder的创建委托 /// token提供者的查找模式 /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "类型httpApiType明确是不会被裁剪的")] +#endif public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder builder, Func handlerFactory, TypeMatchMode tokenProviderSearchMode = TypeMatchMode.TypeOrBaseTypes) where TOAuthTokenHandler : OAuthTokenHandler { diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs index 1cfaa0e4..81751fb3 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 @@ -16,7 +17,11 @@ public static partial class TokenProviderExtensions /// /// TokenProvider的别名 /// - public static OptionsBuilder AddClientCredentialsTokenProvider(this IServiceCollection services, string alias = "") + public static OptionsBuilder AddClientCredentialsTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi>(this IServiceCollection services, string alias = "") { var builder = services.AddTokenProvider(alias); return new OptionsBuilder(builder.Services, builder.Name); @@ -29,7 +34,11 @@ public static OptionsBuilder AddClientCredentialsToken /// /// 配置 /// - public static OptionsBuilder AddClientCredentialsTokenProvider(this IServiceCollection services, Action configureOptions) + public static OptionsBuilder AddClientCredentialsTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi>(this IServiceCollection services, Action configureOptions) { return services.AddClientCredentialsTokenProvider().Configure(configureOptions); } @@ -42,7 +51,11 @@ public static OptionsBuilder AddClientCredentialsToken /// TokenProvider的别名 /// 配置 /// - public static OptionsBuilder AddClientCredentialsTokenProvider(this IServiceCollection services, string alias, Action configureOptions) + public static OptionsBuilder AddClientCredentialsTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + 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..9074425f 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 @@ -16,7 +17,11 @@ public static partial class TokenProviderExtensions /// /// TokenProvider的别名 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider(this IServiceCollection services, string alias = "") + public static OptionsBuilder AddPasswordCredentialsTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi>(this IServiceCollection services, string alias = "") { var builder = services.AddTokenProvider(alias); return new OptionsBuilder(builder.Services, builder.Name); @@ -29,7 +34,11 @@ public static OptionsBuilder AddPasswordCredentialsT /// /// 配置 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider(this IServiceCollection services, Action configureOptions) + public static OptionsBuilder AddPasswordCredentialsTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi>(this IServiceCollection services, Action configureOptions) { return services.AddPasswordCredentialsTokenProvider().Configure(configureOptions); } @@ -42,7 +51,11 @@ public static OptionsBuilder AddPasswordCredentialsT /// TokenProvider的别名 /// 配置 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider(this IServiceCollection services, string alias, Action configureOptions) + public static OptionsBuilder AddPasswordCredentialsTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + 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..98bf3c52 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; @@ -20,7 +21,11 @@ public static partial class TokenProviderExtensions /// token请求委托 /// TokenProvider的别名 /// - public static ITokenProviderBuilder AddTokenProvider(this IServiceCollection services, Func> tokenRequest, string alias = "") + public static ITokenProviderBuilder AddTokenProvider< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi>(this IServiceCollection services, Func> tokenRequest, string alias = "") { return services.AddTokenProvider(s => new DelegateTokenProvider(s, tokenRequest), alias); } @@ -36,11 +41,11 @@ public static ITokenProviderBuilder AddTokenProvider(this IServiceColl /// public static ITokenProviderBuilder AddTokenProvider< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi, #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif TTokenProvider>(this IServiceCollection services, Func tokenProviderFactory, string alias = "") where TTokenProvider : class, ITokenProvider @@ -62,11 +67,11 @@ public static ITokenProviderBuilder AddTokenProvider< /// public static ITokenProviderBuilder AddTokenProvider< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi, #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif TTokenProvider>(this IServiceCollection services, string alias = "") where TTokenProvider : class, ITokenProvider @@ -86,7 +91,15 @@ public static ITokenProviderBuilder AddTokenProvider< /// /// TokenProvider的别名 /// - private static ITokenProviderBuilder AddTokenProviderCore(this IServiceCollection services, string alias) + private static ITokenProviderBuilder AddTokenProviderCore< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + TTokenProvider>(this IServiceCollection services, string alias) where TTokenProvider : class, ITokenProvider { if (alias == null) diff --git a/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs b/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs index 27e7b9fd..d47bc7fa 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 { @@ -14,7 +15,11 @@ public interface ITokenProviderFactory /// 类型匹配模式 /// /// - ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); + ITokenProvider Create( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); /// /// 通过接口类型获取或创建其对应的token提供者 @@ -24,6 +29,10 @@ public interface ITokenProviderFactory /// TokenProvider的别名 /// /// - ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode, string alias); + ITokenProvider Create( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, TypeMatchMode typeMatchMode, string alias); } } diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs index 9a23b1c6..9fe2305e 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; namespace WebApiClientCore.Extensions.OAuths { @@ -33,7 +34,11 @@ public TokenProviderFactory(IServiceProvider serviceProvider, IOptions /// /// - public ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode) + public ITokenProvider Create( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, TypeMatchMode typeMatchMode) { return this.Create(httpApiType, typeMatchMode, alias: string.Empty); } @@ -46,7 +51,11 @@ public ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode) /// TokenProvider的别名 /// /// - public ITokenProvider Create(Type httpApiType, TypeMatchMode typeMatchMode, string alias) + public ITokenProvider Create( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, TypeMatchMode typeMatchMode, string alias) { if (httpApiType == null) { @@ -101,7 +110,11 @@ private ITokenProvider CreateTokenProvider(ServiceKey serviceKey) /// 别名 /// /// - private ITokenProvider? CreateTokenProviderFromBaseType(Type httpApiType, string alias) + private ITokenProvider? CreateTokenProviderFromBaseType( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, string alias) { foreach (var baseType in httpApiType.GetInterfaces()) { @@ -122,13 +135,20 @@ private sealed class ServiceKey : IEquatable { private int? hashCode; +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif public Type HttpApiType { get; } public TypeMatchMode TypeMatchMode { get; } public string Alias { get; } - public ServiceKey(Type httpApiType, TypeMatchMode typeMatchMode, string alias) + public ServiceKey( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, TypeMatchMode typeMatchMode, string alias) { this.HttpApiType = httpApiType; this.TypeMatchMode = typeMatchMode; diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs index 7bb19a34..21e1da75 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore.Extensions.OAuths.Exceptions; @@ -24,6 +25,10 @@ public ClientCredentialsTokenProvider(IServiceProvider services) /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected override Task RequestTokenAsync(IServiceProvider serviceProvider) { var options = this.GetOptionsValue(); @@ -42,6 +47,10 @@ public ClientCredentialsTokenProvider(IServiceProvider services) /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) { var options = this.GetOptionsValue(); diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs index 327e6561..14d2fa61 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -37,6 +38,10 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor [HttpPost] [FormField("grant_type", "client_credentials")] +#if NET8_0_OR_GREATER + [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.")] +#endif public Task RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] ClientCredentials credentials) { return this.PostFormAsync(endpoint, "client_credentials", credentials); @@ -50,6 +55,10 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor [HttpPost] [FormField("grant_type", "password")] +#if NET8_0_OR_GREATER + [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.")] +#endif public Task RequestTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] PasswordCredentials credentials) { return this.PostFormAsync(endpoint, "password", credentials); @@ -63,6 +72,10 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor [HttpPost] [FormField("grant_type", "refresh_token")] +#if NET8_0_OR_GREATER + [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.")] +#endif public Task RefreshTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] RefreshTokenCredentials credentials) { return this.PostFormAsync(endpoint, "refresh_token", credentials); @@ -76,6 +89,10 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor /// /// +#if NET8_0_OR_GREATER + [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.")] +#endif private async Task PostFormAsync(Uri endpoint, string grant_type, TCredentials credentials) { using var formContent = new FormContent(credentials, this.httpApiOptions.KeyValueSerializeOptions); diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs index 7e7c9ef8..1932b665 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using WebApiClientCore.Extensions.OAuths.Exceptions; @@ -24,6 +25,10 @@ public PasswordCredentialsTokenProvider(IServiceProvider services) /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected override Task RequestTokenAsync(IServiceProvider serviceProvider) { var options = this.GetOptionsValue(); @@ -42,6 +47,10 @@ public PasswordCredentialsTokenProvider(IServiceProvider services) /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected override Task RefreshTokenAsync(IServiceProvider serviceProvider, string refresh_token) { var options = this.GetOptionsValue(); diff --git a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj index 34c046b7..0e9ad98e 100644 --- a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj +++ b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj @@ -1,9 +1,10 @@ - enable - netstandard2.1;net5.0 + enable + netstandard2.1;net5.0;net8.0 $(OutputPath)\$(AssemblyName).xml + true true Sign.snk diff --git a/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs b/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs index 8b8d0984..9fab7297 100644 --- a/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs +++ b/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs @@ -13,6 +13,7 @@ public void ToHeaderNameTest() Assert.Equal("Accept", HttpRequestHeader.Accept.ToHeaderName()); Assert.Equal("Accept-Charset", HttpRequestHeader.AcceptCharset.ToHeaderName()); +#if NET5_0_OR_GREATER foreach (var item in Enum.GetValues()) { var name = Enum.GetName(item); @@ -20,6 +21,7 @@ public void ToHeaderNameTest() var headerName = field?.GetCustomAttribute()?.Name; Assert.Equal(headerName, item.ToHeaderName()); } +#endif } } } \ No newline at end of file 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/KeyValueSerializerTest.cs b/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs index 7d3d560d..3de21360 100644 --- a/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs +++ b/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs @@ -12,7 +12,7 @@ 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) @@ -46,9 +46,22 @@ public void SerializeTest() 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 +93,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/ApiParameterContextExtensions.cs b/WebApiClientCore/ApiParameterContextExtensions.cs index 37308e46..05426de9 100644 --- a/WebApiClientCore/ApiParameterContextExtensions.cs +++ b/WebApiClientCore/ApiParameterContextExtensions.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using WebApiClientCore.Internals; using WebApiClientCore.Serialization; @@ -16,6 +17,10 @@ public static class ApiParameterContextExtensions /// /// /// +#if NET8_0_OR_GREATER + [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.")] +#endif public static byte[] SerializeToJson(this ApiParameterContext context) { return context.SerializeToJson(Encoding.UTF8); @@ -27,6 +32,10 @@ public static byte[] SerializeToJson(this ApiParameterContext context) /// /// 编码 /// +#if NET8_0_OR_GREATER + [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.")] +#endif public static byte[] SerializeToJson(this ApiParameterContext context, Encoding encoding) { using var bufferWriter = new RecyclableBufferWriter(); @@ -48,6 +57,10 @@ public static byte[] SerializeToJson(this ApiParameterContext context, Encoding /// /// /// buffer写入器 +#if NET8_0_OR_GREATER + [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.")] +#endif public static void SerializeToJson(this ApiParameterContext context, IBufferWriter bufferWriter) { var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; @@ -60,6 +73,9 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit /// /// xml的编码 /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] +#endif public static string? SerializeToXml(this ApiParameterContext context, Encoding? encoding) { var options = context.HttpContext.HttpApiOptions.XmlSerializeOptions; @@ -77,6 +93,10 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit /// /// /// +#if NET8_0_OR_GREATER + [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.")] +#endif public static IList SerializeToKeyValues(this ApiParameterContext context) { var options = context.HttpContext.HttpApiOptions.KeyValueSerializeOptions; diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 43eae54d..1133d794 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text; using System.Text.Json; @@ -18,6 +19,10 @@ public static class ApiResponseContextExtensions /// /// 目标类型 /// +#if NET8_0_OR_GREATER + [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.")] +#endif public static async Task JsonDeserializeAsync(this ApiResponseContext context, Type objType) { var response = context.HttpContext.ResponseMessage; @@ -61,6 +66,9 @@ public static class ApiResponseContextExtensions /// /// 目标类型 /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] +#endif public static async Task XmlDeserializeAsync(this ApiResponseContext context, Type objType) { var response = context.HttpContext.ResponseMessage; diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs index 9273b2da..002ca7cf 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace WebApiClientCore.Attributes @@ -17,6 +18,10 @@ public class FormContentAttribute : HttpContentAttribute, ICollectionFormatable /// 设置参数到http请求内容 /// /// 上下文 +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected override async Task SetHttpContentAsync(ApiParameterContext context) { var keyValues = this.SerializeToKeyValues(context).CollectAs(this.CollectionFormat); @@ -28,6 +33,10 @@ protected override async Task SetHttpContentAsync(ApiParameterContext context) /// /// 上下文 /// +#if NET8_0_OR_GREATER + [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.")] +#endif 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..7b79d465 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace WebApiClientCore.Attributes @@ -30,6 +31,10 @@ protected override Task SetHttpContentAsync(ApiParameterContext context) /// /// 上下文 /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs index f260069d..a6c47560 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 @@ -21,6 +22,10 @@ public class HeadersAttribute : ApiParameterAttribute /// 上下文 /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public override Task OnRequestAsync(ApiParameterContext context) { foreach (var item in context.SerializeToKeyValues()) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs index fba6dd0f..d8407933 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Threading.Tasks; using WebApiClientCore.HttpContents; @@ -31,6 +32,10 @@ public string CharSet /// /// 上下文 /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif protected override Task SetHttpContentAsync(ApiParameterContext context) { var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs index 50ef7105..ae98dad0 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; using System.Threading.Tasks; namespace WebApiClientCore.Attributes @@ -13,6 +14,10 @@ public class JsonFormDataTextAttribute : ApiParameterAttribute /// /// 上下文 /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public override Task OnRequestAsync(ApiParameterContext context) { var json = context.SerializeToJson(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs index 8d89fb76..92c4453b 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; using System.Threading.Tasks; namespace WebApiClientCore.Attributes @@ -13,6 +14,10 @@ public class JsonFormFieldAttribute : ApiParameterAttribute /// /// 上下文 /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public override async Task OnRequestAsync(ApiParameterContext context) { var json = context.SerializeToJson(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs index 221fb13f..e7e0583e 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; @@ -22,6 +23,10 @@ public class PathQueryAttribute : ApiParameterAttribute, ICollectionFormatable /// 上下文 /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public override Task OnRequestAsync(ApiParameterContext context) { var uri = context.HttpContext.RequestMessage.RequestUri; @@ -40,6 +45,10 @@ public override Task OnRequestAsync(ApiParameterContext context) /// /// 上下文 /// +#if NET8_0_OR_GREATER + [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.")] +#endif protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs index b56e050f..2011e40b 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Threading.Tasks; using WebApiClientCore.HttpContents; @@ -30,6 +31,9 @@ public string CharSet /// /// 上下文 /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] +#endif protected override Task SetHttpContentAsync(ApiParameterContext context) { var xml = context.SerializeToXml(this.encoding); diff --git a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs index 3b1cb1df..f901c73f 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs @@ -1,4 +1,5 @@ -using System.Net.Http.Headers; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Threading.Tasks; using WebApiClientCore.HttpContents; using WebApiClientCore.Internals; @@ -54,7 +55,11 @@ protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseCo /// 设置强类型模型结果值 /// /// 上下文 - /// + /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public override async Task SetResultAsync(ApiResponseContext context) { var resultType = context.ActionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs index 8444c822..08a37578 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs @@ -1,4 +1,5 @@ -using System.Net.Http.Headers; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Threading.Tasks; using WebApiClientCore.HttpContents; using WebApiClientCore.Internals; @@ -40,7 +41,7 @@ public XmlReturnAttribute(double acceptQuality) /// protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseContentType) { - return base.IsMatchAcceptContentType(responseContentType) + return base.IsMatchAcceptContentType(responseContentType) || MediaTypeUtil.IsMatch(textXml, responseContentType.MediaType); } @@ -49,6 +50,9 @@ protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseCo /// /// 上下文 /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] +#endif public override async Task SetResultAsync(ApiResponseContext context) { var resultType = context.ActionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/BuildinExtensions/TypeExtensions.cs b/WebApiClientCore/BuildinExtensions/TypeExtensions.cs index 830eccd6..57b98a27 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,7 +62,11 @@ public static bool IsInheritFrom(this Type type) /// 参数值 /// /// - public static T CreateInstance(this Type type, params object?[] args) + public static T CreateInstance( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + this Type type, params object?[] args) { var instance = Activator.CreateInstance(type, args); if (instance == null) @@ -76,7 +81,11 @@ public static T CreateInstance(this Type type, params object?[] args) /// /// 接口类型 /// - public static Attribute[] GetInterfaceCustomAttributes(this Type interfaceType) + public static Attribute[] GetInterfaceCustomAttributes( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + this Type interfaceType) { return interfaceType .GetInterfaces() diff --git a/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs b/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs index 2ccbc129..3b5807e3 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; @@ -20,7 +21,7 @@ public static class HttpApiExtensions /// public static IHttpClientBuilder AddHttpApi< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi>(this IServiceCollection services) where THttpApi : class { @@ -47,7 +48,7 @@ public static IHttpClientBuilder AddHttpApi< /// public static IHttpClientBuilder AddHttpApi< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi>(this IServiceCollection services, Action configureOptions) where THttpApi : class { @@ -65,7 +66,7 @@ public static IHttpClientBuilder AddHttpApi< /// public static IHttpClientBuilder AddHttpApi< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi>(this IServiceCollection services, Action configureOptions) where THttpApi : class { @@ -82,7 +83,11 @@ public static IHttpClientBuilder AddHttpApi< /// 接口类型 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Type httpApiType) + public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType) { if (httpApiType == null) { @@ -105,7 +110,11 @@ 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, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, Action configureOptions) { return services .AddHttpApi(httpApiType) @@ -120,7 +129,11 @@ 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, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, Action configureOptions) { return services .AddHttpApi(httpApiType) @@ -135,7 +148,7 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, Ty /// private class HttpApiProvider< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi> { @@ -190,11 +203,15 @@ private abstract class HttpApiAdder /// /// 接口类型 /// - #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors, typeof(HttpApiAdderOf<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(HttpApiAdderOf<>))] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] +#endif + public static HttpApiAdder Create( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif - public static HttpApiAdder Create(Type httpApiType) + Type httpApiType) { var adderType = typeof(HttpApiAdderOf<>).MakeGenericType(httpApiType); return adderType.CreateInstance(); @@ -206,7 +223,7 @@ public static HttpApiAdder Create(Type httpApiType) /// private class HttpApiAdderOf< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi> : HttpApiAdder where THttpApi : class { diff --git a/WebApiClientCore/HttpApi.cs b/WebApiClientCore/HttpApi.cs index 577f4779..af39d83a 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; @@ -91,7 +92,14 @@ private static void GetName(Type type, ref ValueStringBuilder builder) /// /// /// - public static MethodInfo[] FindApiMethods(Type httpApiType) +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "已使用DynamicallyAccessedMembers.All关联接口的父接口成员")] +#endif + public static MethodInfo[] FindApiMethods( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType) { if (httpApiType.IsInterface == false) { diff --git a/WebApiClientCore/HttpContents/FormContent.cs b/WebApiClientCore/HttpContents/FormContent.cs index c3836616..d7ef2fec 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; @@ -56,6 +57,10 @@ public FormContent(IEnumerable keyValues) /// /// 模型对象值 /// 序列化选项 +#if NET8_0_OR_GREATER + [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.")] +#endif public FormContent(object? value, KeyValueSerializerOptions? options) { if (value != null) diff --git a/WebApiClientCore/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index 83d5c6cc..2023647f 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; using System.Text.Json; using WebApiClientCore.Internals; using WebApiClientCore.Serialization; @@ -28,6 +29,10 @@ public JsonContent() /// /// 对象值 /// json序列化选项 +#if NET8_0_OR_GREATER + [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.")] +#endif public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) : this(value, jsonSerializerOptions, null) { @@ -39,6 +44,10 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) /// 对象值 /// json序列化选项 /// 编码 +#if NET8_0_OR_GREATER + [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.")] +#endif public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions, Encoding? encoding) : base(MediaType) { diff --git a/WebApiClientCore/HttpContents/JsonPatchContent.cs b/WebApiClientCore/HttpContents/JsonPatchContent.cs index 132a4b11..9cff4f8c 100644 --- a/WebApiClientCore/HttpContents/JsonPatchContent.cs +++ b/WebApiClientCore/HttpContents/JsonPatchContent.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using WebApiClientCore.Serialization; @@ -27,6 +28,10 @@ public JsonPatchContent() /// /// patch操作项 /// json序列化选项 +#if NET8_0_OR_GREATER + [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.")] +#endif public JsonPatchContent(IEnumerable operations, JsonSerializerOptions? jsonSerializerOptions) : base(MediaType) { diff --git a/WebApiClientCore/Implementations/DataValidator.cs b/WebApiClientCore/Implementations/DataValidator.cs index 26ccd72b..4834a70a 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,9 @@ static class DataValidator /// 参数值 /// 是否验证属性值 /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许instance属性被裁剪")] +#endif public static void ValidateParameter(ApiParameterDescriptor parameter, object? parameterValue, bool validateProperty) { var name = parameter.Name; @@ -44,6 +48,9 @@ public static void ValidateParameter(ApiParameterDescriptor parameter, object? p /// /// 结果值 /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许instance属性被裁剪")] +#endif public static void ValidateReturnValue(object? value) { if (value != null && IsNeedValidateProperty(value) == true) @@ -58,6 +65,9 @@ public static void ValidateReturnValue(object? value) /// /// 实例 /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "允许instance属性被裁剪")] +#endif private static bool IsNeedValidateProperty(object instance) { var type = instance.GetType(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs index 9ba4fc5d..5f691160 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,9 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// 获取所在接口类型 /// 这个值不一定是声明方法的接口类型 /// +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif public override Type InterfaceType { get; protected set; } /// @@ -65,6 +69,9 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// for test only /// /// 接口的方法 +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "允许DeclaringType被裁剪")] +#endif internal DefaultApiActionDescriptor(MethodInfo method) : this(method, method.DeclaringType!) { @@ -75,7 +82,11 @@ internal DefaultApiActionDescriptor(MethodInfo method) /// /// 接口的方法 /// 接口类型 - public DefaultApiActionDescriptor(MethodInfo method, Type interfaceType) + public DefaultApiActionDescriptor(MethodInfo method, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type interfaceType) { var methodAttributes = method.GetCustomAttributes().ToArray(); var interfaceAttributes = interfaceType.GetInterfaceCustomAttributes(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs index f6bd2779..922b2848 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,11 @@ public class DefaultApiActionDescriptorProvider : IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - public virtual ApiActionDescriptor CreateActionDescriptor(MethodInfo method, Type interfaceType) + public virtual ApiActionDescriptor CreateActionDescriptor(MethodInfo method, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type interfaceType) { return new DefaultApiActionDescriptor(method, interfaceType); } diff --git a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs index 706f7b1d..db909550 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 { @@ -31,7 +32,8 @@ public ApiActionInvoker CreateActionInvoker(ApiActionDescriptor actionDescriptor /// Action描述 /// #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors, typeof(DefaultApiActionInvoker<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(DefaultApiActionInvoker<>))] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] #endif protected virtual ApiActionInvoker CreateDefaultActionInvoker(ApiActionDescriptor actionDescriptor) { diff --git a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs index 5e5b7afe..6408d6ce 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,9 @@ public class DefaultApiReturnDescriptor : ApiReturnDescriptor /// for test only /// /// 方法信息 +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "允许DeclaringType被裁剪")] +#endif internal DefaultApiReturnDescriptor(MethodInfo method) : this(method, method.DeclaringType!) { @@ -44,7 +48,11 @@ internal DefaultApiReturnDescriptor(MethodInfo method) /// /// 方法信息 /// 接口类型 - public DefaultApiReturnDescriptor(MethodInfo method, Type interfaceType) + public DefaultApiReturnDescriptor(MethodInfo method, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type interfaceType) : this(method.ReturnType, method.GetCustomAttributes(), interfaceType.GetInterfaceCustomAttributes()) { } diff --git a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs index 26c83dea..37c9380a 100644 --- a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace WebApiClientCore.Implementations { @@ -10,7 +11,7 @@ namespace WebApiClientCore.Implementations /// public class DefaultHttpApiActivator< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi> : IHttpApiActivator { @@ -23,6 +24,9 @@ public class DefaultHttpApiActivator< /// /// /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] +#endif public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { this.httpApiActivator = SourceGeneratorHttpApiActivator.IsSupported diff --git a/WebApiClientCore/Implementations/HttpApiActivator.cs b/WebApiClientCore/Implementations/HttpApiActivator.cs index 3dc782e2..55243d9d 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,11 @@ namespace WebApiClientCore.Implementations /// /// [Obsolete("该类型存在构造器调用虚方法的设计失误,不建议再使用", error: false)] - public abstract class HttpApiActivator : IHttpApiActivator + public abstract class HttpApiActivator< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + THttpApi> : IHttpApiActivator { /// /// 接口的所有方法执行器 diff --git a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs index 9d8e8119..2b94eb9a 100644 --- a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.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; @@ -13,7 +14,7 @@ namespace WebApiClientCore.Implementations /// public sealed class ILEmitHttpApiActivator< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi> : IHttpApiActivator { @@ -27,6 +28,12 @@ public sealed class ILEmitHttpApiActivator< /// /// /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "proxyType是运行时生成的")] +#endif +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Calls System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)")] +#endif public ILEmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var apiMethods = HttpApi.FindApiMethods(typeof(THttpApi)); @@ -36,7 +43,7 @@ public ILEmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorPr .Select(actionInvokerProvider.CreateActionInvoker) .ToArray(); - var proxyType = BuildProxyType(typeof(THttpApi), apiMethods); + var proxyType = BuildProxyType(apiMethods); this.activator = LambdaUtil.CreateCtorFunc(proxyType); } @@ -66,15 +73,18 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// /// 创建IHttpApi代理类的类型 - /// - /// 接口类型 + /// /// 接口的方法 /// /// /// - private static Type BuildProxyType(Type interfaceType, MethodInfo[] apiMethods) +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Calls System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)")] +#endif + private static Type BuildProxyType(MethodInfo[] apiMethods) { // 接口的实现在动态程序集里,所以接口必须为public修饰才可以创建代理类并实现此接口 + var interfaceType = typeof(THttpApi); if (interfaceType.IsVisible == false) { var message = Resx.required_PublicInterface.Format(interfaceType); diff --git a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs index aeb09f0f..0e5352f0 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,11 @@ public class JsonFirstApiActionDescriptor : DefaultApiActionDescriptor /// /// /// - public JsonFirstApiActionDescriptor(MethodInfo method, Type interfaceType) + public JsonFirstApiActionDescriptor(MethodInfo method, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type interfaceType) : base(method, interfaceType) { var defineGetHead = this.Attributes.Any(a => this.IsGetHeadAttribute(a)); diff --git a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs index 96bb7f10..13942b45 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,11 @@ public class JsonFirstApiActionDescriptorProvider : IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - public ApiActionDescriptor CreateActionDescriptor(MethodInfo method, Type interfaceType) + public ApiActionDescriptor CreateActionDescriptor(MethodInfo method, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type interfaceType) { return new JsonFirstApiActionDescriptor(method, interfaceType); } diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index ec42f820..4c44ca5c 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using WebApiClientCore.Exceptions; @@ -15,7 +16,7 @@ namespace WebApiClientCore.Implementations /// public sealed class SourceGeneratorHttpApiActivator< #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] #endif THttpApi> : IHttpApiActivator { @@ -36,6 +37,9 @@ public sealed class SourceGeneratorHttpApiActivator< /// /// /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "类型proxyClassType已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] +#endif public SourceGeneratorHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var httpApiType = typeof(THttpApi); @@ -70,7 +74,15 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// 接口类型 /// 接口的实现类型 /// - private static IEnumerable FindApiMethods(Type httpApiType, Type proxyClassType) + private static IEnumerable FindApiMethods( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type httpApiType, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + Type proxyClassType) { var apiMethods = HttpApi.FindApiMethods(httpApiType) .Select(item => new MethodFeature(item, isProxyMethod: false)) diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs index f14fb4e4..0b546a5a 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace WebApiClientCore.Implementations @@ -36,7 +37,10 @@ static class SourceGeneratorProxyClassFinder return null; } - +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "类型已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075", Justification = "类型已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] +#endif private static void AnalyzeAssembly(Assembly assembly) { var httpApiProxyClass = assembly.GetType(HttpApiProxyClassTypeName); diff --git a/WebApiClientCore/Internals/LambdaUtil.cs b/WebApiClientCore/Internals/LambdaUtil.cs index 65647349..7c15829e 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 ArgumentNullException(nameof(property)); } // (TDeclaring instance) => (propertyType)((declaringType)instance).propertyName - var paramInstance = Expression.Parameter(typeof(TDeclaring)); var bodyInstance = (Expression)paramInstance; @@ -105,15 +81,14 @@ 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)); } return Expression.Lambda>(bodyProperty, paramInstance).Compile(); - } - + } /// /// 创建字段的获取委托 @@ -156,7 +131,11 @@ public static Func CreateGetFunc(FieldIn /// /// 类型 /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type) { return CreateCtorFunc>(type, Array.Empty()); } @@ -170,7 +149,11 @@ public static Func CreateCtorFunc(Type type) /// /// /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type) { var args = new Type[] { typeof(TArg1) }; return CreateCtorFunc>(type, args); @@ -186,7 +169,11 @@ public static Func CreateCtorFunc(Type type) /// /// /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type) { var args = new Type[] { typeof(TArg1), typeof(TArg2) }; return CreateCtorFunc>(type, args); @@ -201,7 +188,11 @@ public static Func CreateCtorFunc(Type /// /// /// - public static Func CreateCtorFunc(Type type) + public static Func CreateCtorFunc( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type) { var args = new Type[] { typeof(TArg1), typeof(TArg2), typeof(TArg3) }; return CreateCtorFunc>(type, args); @@ -216,7 +207,11 @@ public static Func CreateCtorFunc /// /// - private static TFunc CreateCtorFunc(Type type, Type[] args) + private static TFunc CreateCtorFunc( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type, Type[] args) { if (type == null) { diff --git a/WebApiClientCore/Parameters/JsonPatchDocument.cs b/WebApiClientCore/Parameters/JsonPatchDocument.cs index 267f214a..f44433a2 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading.Tasks; using WebApiClientCore.Exceptions; @@ -68,6 +69,10 @@ public void Replace(string path, object? value) /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public Task OnRequestAsync(ApiParameterContext context) { if (context.HttpContext.RequestMessage.Method != HttpMethod.Patch) diff --git a/WebApiClientCore/Serialization/JsonBufferSerializer.cs b/WebApiClientCore/Serialization/JsonBufferSerializer.cs index 44771922..6fff704e 100644 --- a/WebApiClientCore/Serialization/JsonBufferSerializer.cs +++ b/WebApiClientCore/Serialization/JsonBufferSerializer.cs @@ -1,4 +1,6 @@ -using System.Buffers; +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace WebApiClientCore.Serialization @@ -19,6 +21,10 @@ public static class JsonBufferSerializer /// buffer写入器 /// 对象 /// 选项 +#if NET8_0_OR_GREATER + [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.")] +#endif public static void Serialize(IBufferWriter bufferWriter, object? obj, JsonSerializerOptions? options) { if (obj == null) diff --git a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs index 94bbdea6..0a9b534d 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; @@ -32,7 +33,8 @@ public override bool CanConvert(Type typeToConvert) /// /// #if NET5_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors, typeof(Converter<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(Converter<>))] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] #endif public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { @@ -53,6 +55,11 @@ private class Converter : JsonConverter where TJsonStr /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var json = reader.GetString(); @@ -67,6 +74,10 @@ public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, /// /// /// +#if NET5_0_OR_GREATER + [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 = "")] +#endif 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..ff489302 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,10 @@ public static class KeyValueSerializer /// 对象实例 /// 选项 /// +#if NET8_0_OR_GREATER + [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.")] +#endif public static IList Serialize(string key, object? obj, KeyValueSerializerOptions? options) { var kvOptions = options ?? defaultOptions; @@ -73,6 +78,10 @@ public static IList Serialize(string key, object? obj, KeyValueSeriali /// 对象类型 /// 选项 /// +#if NET8_0_OR_GREATER + [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.")] +#endif private static IList GetKeyValueList(string key, object obj, Type objType, KeyValueSerializerOptions options) { var jsonOptions = options.GetJsonSerializerOptions(); diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index ee3c6e59..1dedbe81 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Xml; @@ -19,6 +20,9 @@ public static class XmlSerializer /// 对象 /// 配置选项 /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] +#endif public static string? Serialize(object? obj, XmlWriterSettings? options) { if (obj == null) @@ -41,6 +45,9 @@ public static class XmlSerializer /// 对象类型 /// 配置选项 /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] +#endif public static object? Deserialize(string? xml, Type objType, XmlReaderSettings? options) { if (objType == null) diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 5dc5e3a5..1f4102e0 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -1,10 +1,11 @@  - enable - netstandard2.1;net5.0 + enable + netstandard2.1;net5.0;net8.0 $(OutputPath)\$(AssemblyName).xml - + true + .NetCore声明式的Http客户端库 一款基于HttpClient封装,只需要定义c#接口并修饰相关特性,即可异步调用远程http接口的客户端库 From ac112e0c20d6039019469ce5ffa01d5cd028f615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Sat, 15 Jun 2024 14:46:55 +0800 Subject: [PATCH 048/108] =?UTF-8?q?IsBuffered()=E7=9A=84UnsafeAccessor?= =?UTF-8?q?=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiResponseContextExtensions.cs | 4 +-- .../System.Net.Http/HttpContentExtensions.cs | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 1133d794..4cf4052d 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -54,9 +54,7 @@ public static class ApiResponseContextExtensions else { var utf8Json = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - return utf8Json.Length == 0 - ? objType.DefaultValue() - : JsonSerializer.Deserialize(utf8Json, objType, options); + return utf8Json.Length == 0 ? objType.DefaultValue() : JsonSerializer.Deserialize(utf8Json, objType, options); } } diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index d0ecbd5d..3d3dddc6 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using WebApiClientCore.Exceptions; @@ -11,23 +12,40 @@ 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; /// /// 静态构造器 /// 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,7 +53,7 @@ static HttpContentExtensions() /// public static bool? IsBuffered(this HttpContent httpContent) { - return isBuffered?.Invoke(httpContent); + return isBufferedFunc == null ? null : isBufferedFunc(httpContent); } /// From fa9e933dc900d05409a34d8b690bfdaa9a34b1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Sat, 15 Jun 2024 15:38:08 +0800 Subject: [PATCH 049/108] =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiActionDescriptor.cs | 4 +- .../DynamicDependencyAttribute.cs | 32 +++++++ .../DynamicallyAccessedMemberTypes.cs | 85 +++++++++++++++++++ .../DynamicallyAccessedMembersAttribute.cs | 25 ++++++ .../UnconditionalSuppressMessageAttribute.cs | 52 ++++++++++++ .../IApiActionDescriptorProvider.cs | 8 +- .../IHttpApiActivator.cs | 6 +- .../DynamicDependencyAttribute.cs | 32 +++++++ .../DynamicallyAccessedMemberTypes.cs | 85 +++++++++++++++++++ .../DynamicallyAccessedMembersAttribute.cs | 25 ++++++ .../UnconditionalSuppressMessageAttribute.cs | 52 ++++++++++++ ...TokenProviderExtensions.GrantTypeClient.cs | 27 +++--- ...kenProviderExtensions.GrantTypePassword.cs | 25 +++--- .../TokenProviderExtensions.cs | 49 ++++------- .../ITokenProviderFactory.cs | 13 ++- .../TokenProviderFactory.cs | 28 +++--- .../ApiParameterContextExtensions.cs | 10 --- .../ApiResponseContextExtensions.cs | 4 - .../FormContentAttribute.cs | 4 - .../FormDataContentAttribute.cs | 4 +- .../ParameterAttributes/HeadersAttribute.cs | 2 - .../JsonContentAttribute.cs | 2 - .../JsonFormDataTextAttribute.cs | 2 - .../JsonFormFieldAttribute.cs | 2 - .../ParameterAttributes/PathQueryAttribute.cs | 4 - .../XmlContentAttribute.cs | 2 - .../ReturnAttributes/JsonReturnAttribute.cs | 2 - .../ReturnAttributes/XmlReturnAttribute.cs | 2 - .../BuildinExtensions/TypeExtensions.cs | 11 +-- .../DynamicDependencyAttribute.cs | 32 +++++++ .../DynamicallyAccessedMemberTypes.cs | 85 +++++++++++++++++++ .../DynamicallyAccessedMembersAttribute.cs | 25 ++++++ .../RequiresDynamicCodeAttribute.cs | 25 ++++++ .../RequiresUnreferencedCodeAttribute.cs | 22 +++++ .../UnconditionalSuppressMessageAttribute.cs | 52 ++++++++++++ .../DependencyInjection/HttpApiExtensions.cs | 70 +++++---------- WebApiClientCore/HttpApi.cs | 10 +-- WebApiClientCore/HttpContents/FormContent.cs | 2 - WebApiClientCore/HttpContents/JsonContent.cs | 4 - .../HttpContents/JsonPatchContent.cs | 2 - .../Implementations/DataValidator.cs | 6 -- .../DefaultApiActionDescriptor.cs | 15 ++-- .../DefaultApiActionDescriptorProvider.cs | 8 +- .../DefaultApiActionInvokerProvider.cs | 4 +- .../DefaultApiReturnDescriptor.cs | 10 +-- .../DefaultHttpApiActivator.cs | 9 +- .../Implementations/HttpApiActivator.cs | 7 +- .../Implementations/ILEmitHttpApiActivator.cs | 13 +-- .../JsonFirstApiActionDescriptor.cs | 8 +- .../JsonFirstApiActionDescriptorProvider.cs | 8 +- .../SourceGeneratorHttpApiActivator.cs | 19 +---- .../SourceGeneratorProxyClassFinder.cs | 2 - WebApiClientCore/Internals/LambdaUtil.cs | 29 ++----- .../Parameters/JsonPatchDocument.cs | 2 - .../Serialization/JsonBufferSerializer.cs | 2 - .../JsonConverters/JsonStringTypeConverter.cs | 8 +- .../Serialization/KeyValueSerializer.cs | 4 - .../Serialization/XmlSerializer.cs | 4 - 58 files changed, 753 insertions(+), 333 deletions(-) create mode 100644 WebApiClientCore.Abstractions/CodeAnalysis/DynamicDependencyAttribute.cs create mode 100644 WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMemberTypes.cs create mode 100644 WebApiClientCore.Abstractions/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs create mode 100644 WebApiClientCore.Abstractions/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs create mode 100644 WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicDependencyAttribute.cs create mode 100644 WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMemberTypes.cs create mode 100644 WebApiClientCore.Extensions.OAuths/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs create mode 100644 WebApiClientCore.Extensions.OAuths/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs create mode 100644 WebApiClientCore/CodeAnalysis/DynamicDependencyAttribute.cs create mode 100644 WebApiClientCore/CodeAnalysis/DynamicallyAccessedMemberTypes.cs create mode 100644 WebApiClientCore/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs create mode 100644 WebApiClientCore/CodeAnalysis/RequiresDynamicCodeAttribute.cs create mode 100644 WebApiClientCore/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs create mode 100644 WebApiClientCore/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs diff --git a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs index cd981970..53a39feb 100644 --- a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs +++ b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs @@ -14,10 +14,8 @@ public abstract class ApiActionDescriptor /// /// 获取所在接口类型 /// 这个值不一定是声明方法的接口类型 - /// -#if NET5_0_OR_GREATER + /// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif public abstract Type InterfaceType { 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/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/IApiActionDescriptorProvider.cs b/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs index c6748e81..7580d578 100644 --- a/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs +++ b/WebApiClientCore.Abstractions/IApiActionDescriptorProvider.cs @@ -14,10 +14,8 @@ public interface IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - ApiActionDescriptor CreateActionDescriptor(MethodInfo method, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type interfaceType); + ApiActionDescriptor CreateActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType); } } diff --git a/WebApiClientCore.Abstractions/IHttpApiActivator.cs b/WebApiClientCore.Abstractions/IHttpApiActivator.cs index 1d6ec50f..9e7a93bd 100644 --- a/WebApiClientCore.Abstractions/IHttpApiActivator.cs +++ b/WebApiClientCore.Abstractions/IHttpApiActivator.cs @@ -6,11 +6,7 @@ namespace WebApiClientCore /// 定义THttpApi的实例创建器的接口 /// /// - public interface IHttpApiActivator< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> + public interface IHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> { /// /// 创建THttpApi的代理实例 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/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/TokenProviderExtensions.GrantTypeClient.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs index 81751fb3..3e54015a 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs @@ -17,12 +17,10 @@ public static partial class TokenProviderExtensions /// /// TokenProvider的别名 /// - public static OptionsBuilder AddClientCredentialsTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(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); } @@ -34,11 +32,9 @@ public static OptionsBuilder AddClientCredentialsToken /// /// 配置 /// - public static OptionsBuilder AddClientCredentialsTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(this IServiceCollection services, Action configureOptions) + public static OptionsBuilder AddClientCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Action configureOptions) { return services.AddClientCredentialsTokenProvider().Configure(configureOptions); } @@ -51,11 +47,10 @@ public static OptionsBuilder AddClientCredentialsToken /// TokenProvider的别名 /// 配置 /// - public static OptionsBuilder AddClientCredentialsTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(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 9074425f..bb5aa2df 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs @@ -17,11 +17,9 @@ public static partial class TokenProviderExtensions /// /// TokenProvider的别名 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(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); @@ -34,11 +32,9 @@ public static OptionsBuilder AddPasswordCredentialsT /// /// 配置 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(this IServiceCollection services, Action configureOptions) + public static OptionsBuilder AddPasswordCredentialsTokenProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi>( + this IServiceCollection services, + Action configureOptions) { return services.AddPasswordCredentialsTokenProvider().Configure(configureOptions); } @@ -51,11 +47,10 @@ public static OptionsBuilder AddPasswordCredentialsT /// TokenProvider的别名 /// 配置 /// - public static OptionsBuilder AddPasswordCredentialsTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(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 98bf3c52..c22442eb 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs @@ -21,11 +21,10 @@ public static partial class TokenProviderExtensions /// token请求委托 /// TokenProvider的别名 /// - public static ITokenProviderBuilder AddTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi>(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); } @@ -40,15 +39,11 @@ public static ITokenProviderBuilder AddTokenProvider< /// TokenProvider的别名 /// public static ITokenProviderBuilder AddTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(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() @@ -66,15 +61,10 @@ public static ITokenProviderBuilder AddTokenProvider< /// TokenProvider的别名 /// public static ITokenProviderBuilder AddTokenProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(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() @@ -92,15 +82,10 @@ public static ITokenProviderBuilder AddTokenProvider< /// TokenProvider的别名 /// private static ITokenProviderBuilder AddTokenProviderCore< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(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 { if (alias == null) { diff --git a/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs b/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs index d47bc7fa..965ab5dd 100644 --- a/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs @@ -16,10 +16,8 @@ public interface ITokenProviderFactory /// /// ITokenProvider Create( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); /// /// 通过接口类型获取或创建其对应的token提供者 @@ -30,9 +28,8 @@ ITokenProvider Create( /// /// ITokenProvider Create( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, TypeMatchMode typeMatchMode, string alias); + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode, + string alias); } } diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs index 9fe2305e..ed04f04e 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs @@ -35,10 +35,8 @@ public TokenProviderFactory(IServiceProvider serviceProvider, IOptions /// public ITokenProvider Create( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, TypeMatchMode typeMatchMode) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly) { return this.Create(httpApiType, typeMatchMode, alias: string.Empty); } @@ -52,10 +50,9 @@ public ITokenProvider Create( /// /// public ITokenProvider Create( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, TypeMatchMode typeMatchMode, string alias) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode, + string alias) { if (httpApiType == null) { @@ -111,10 +108,8 @@ private ITokenProvider CreateTokenProvider(ServiceKey serviceKey) /// /// private ITokenProvider? CreateTokenProviderFromBaseType( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, string alias) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + string alias) { foreach (var baseType in httpApiType.GetInterfaces()) { @@ -135,9 +130,7 @@ private sealed class ServiceKey : IEquatable { private int? hashCode; -#if NET5_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif public Type HttpApiType { get; } public TypeMatchMode TypeMatchMode { get; } @@ -145,10 +138,9 @@ private sealed class ServiceKey : IEquatable public string Alias { get; } public ServiceKey( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, TypeMatchMode typeMatchMode, string alias) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + TypeMatchMode typeMatchMode, + string alias) { this.HttpApiType = httpApiType; this.TypeMatchMode = typeMatchMode; diff --git a/WebApiClientCore/ApiParameterContextExtensions.cs b/WebApiClientCore/ApiParameterContextExtensions.cs index 05426de9..02224825 100644 --- a/WebApiClientCore/ApiParameterContextExtensions.cs +++ b/WebApiClientCore/ApiParameterContextExtensions.cs @@ -17,10 +17,8 @@ public static class ApiParameterContextExtensions /// /// /// -#if NET8_0_OR_GREATER [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.")] -#endif public static byte[] SerializeToJson(this ApiParameterContext context) { return context.SerializeToJson(Encoding.UTF8); @@ -32,10 +30,8 @@ public static byte[] SerializeToJson(this ApiParameterContext context) /// /// 编码 /// -#if NET8_0_OR_GREATER [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.")] -#endif public static byte[] SerializeToJson(this ApiParameterContext context, Encoding encoding) { using var bufferWriter = new RecyclableBufferWriter(); @@ -57,10 +53,8 @@ public static byte[] SerializeToJson(this ApiParameterContext context, Encoding /// /// /// buffer写入器 -#if NET8_0_OR_GREATER [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.")] -#endif public static void SerializeToJson(this ApiParameterContext context, IBufferWriter bufferWriter) { var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; @@ -73,9 +67,7 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit /// /// xml的编码 /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] -#endif public static string? SerializeToXml(this ApiParameterContext context, Encoding? encoding) { var options = context.HttpContext.HttpApiOptions.XmlSerializeOptions; @@ -93,10 +85,8 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit /// /// /// -#if NET8_0_OR_GREATER [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.")] -#endif public static IList SerializeToKeyValues(this ApiParameterContext context) { var options = context.HttpContext.HttpApiOptions.KeyValueSerializeOptions; diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 4cf4052d..8b6e761f 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -19,10 +19,8 @@ public static class ApiResponseContextExtensions /// /// 目标类型 /// -#if NET8_0_OR_GREATER [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.")] -#endif public static async Task JsonDeserializeAsync(this ApiResponseContext context, Type objType) { var response = context.HttpContext.ResponseMessage; @@ -64,9 +62,7 @@ public static class ApiResponseContextExtensions /// /// 目标类型 /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] -#endif public static async Task XmlDeserializeAsync(this ApiResponseContext context, Type objType) { var response = context.HttpContext.ResponseMessage; diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs index 002ca7cf..6f8c900c 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs @@ -18,10 +18,8 @@ public class FormContentAttribute : HttpContentAttribute, ICollectionFormatable /// 设置参数到http请求内容 /// /// 上下文 -#if NET5_0_OR_GREATER [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 = "")] -#endif protected override async Task SetHttpContentAsync(ApiParameterContext context) { var keyValues = this.SerializeToKeyValues(context).CollectAs(this.CollectionFormat); @@ -33,10 +31,8 @@ protected override async Task SetHttpContentAsync(ApiParameterContext context) /// /// 上下文 /// -#if NET8_0_OR_GREATER [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.")] -#endif protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs index 7b79d465..e608fb65 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs @@ -30,11 +30,9 @@ protected override Task SetHttpContentAsync(ApiParameterContext context) /// 序列化参数为keyValue /// /// 上下文 - /// -#if NET5_0_OR_GREATER + /// [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 = "")] -#endif protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs index a6c47560..4fadb12e 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs @@ -22,10 +22,8 @@ public class HeadersAttribute : ApiParameterAttribute /// 上下文 /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public override Task OnRequestAsync(ApiParameterContext context) { foreach (var item in context.SerializeToKeyValues()) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs index d8407933..6f4fe2a2 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -32,10 +32,8 @@ public string CharSet /// /// 上下文 /// -#if NET5_0_OR_GREATER [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 = "")] -#endif protected override Task SetHttpContentAsync(ApiParameterContext context) { var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs index ae98dad0..be0c7dc7 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs @@ -14,10 +14,8 @@ public class JsonFormDataTextAttribute : ApiParameterAttribute /// /// 上下文 /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public override Task OnRequestAsync(ApiParameterContext context) { var json = context.SerializeToJson(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs index 92c4453b..aec5e366 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs @@ -14,10 +14,8 @@ public class JsonFormFieldAttribute : ApiParameterAttribute /// /// 上下文 /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public override async Task OnRequestAsync(ApiParameterContext context) { var json = context.SerializeToJson(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs index e7e0583e..ae766830 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs @@ -23,10 +23,8 @@ public class PathQueryAttribute : ApiParameterAttribute, ICollectionFormatable /// 上下文 /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public override Task OnRequestAsync(ApiParameterContext context) { var uri = context.HttpContext.RequestMessage.RequestUri; @@ -45,10 +43,8 @@ public override Task OnRequestAsync(ApiParameterContext context) /// /// 上下文 /// -#if NET8_0_OR_GREATER [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.")] -#endif protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext context) { return context.SerializeToKeyValues(); diff --git a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs index 2011e40b..abe560bf 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs @@ -31,9 +31,7 @@ public string CharSet /// /// 上下文 /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] -#endif protected override Task SetHttpContentAsync(ApiParameterContext context) { var xml = context.SerializeToXml(this.encoding); diff --git a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs index f901c73f..f89bbfd8 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs @@ -56,10 +56,8 @@ protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseCo /// /// 上下文 /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public override async Task SetResultAsync(ApiResponseContext context) { var resultType = context.ActionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs index 08a37578..b5dfacbf 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs @@ -50,9 +50,7 @@ protected override bool IsMatchAcceptContentType(MediaTypeHeaderValue responseCo /// /// 上下文 /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] -#endif public override async Task SetResultAsync(ApiResponseContext context) { var resultType = context.ActionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/BuildinExtensions/TypeExtensions.cs b/WebApiClientCore/BuildinExtensions/TypeExtensions.cs index 57b98a27..dc8c0864 100644 --- a/WebApiClientCore/BuildinExtensions/TypeExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/TypeExtensions.cs @@ -63,10 +63,9 @@ public static bool IsInheritFrom(this Type type) /// /// public static T CreateInstance( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - this Type type, params object?[] args) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + this Type type, + params object?[] args) { var instance = Activator.CreateInstance(type, args); if (instance == null) @@ -82,9 +81,7 @@ public static T CreateInstance( /// 接口类型 /// public static Attribute[] GetInterfaceCustomAttributes( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] this Type interfaceType) { return interfaceType 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/HttpApiExtensions.cs b/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs index 3b5807e3..db17f87f 100644 --- a/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs +++ b/WebApiClientCore/DependencyInjection/HttpApiExtensions.cs @@ -19,11 +19,8 @@ public static class HttpApiExtensions /// /// /// - public static IHttpClientBuilder AddHttpApi< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#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)); @@ -46,11 +43,9 @@ public static IHttpClientBuilder AddHttpApi< /// /// 配置选项 /// - public static IHttpClientBuilder AddHttpApi< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#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() @@ -64,11 +59,9 @@ public static IHttpClientBuilder AddHttpApi< /// /// 配置选项 /// - public static IHttpClientBuilder AddHttpApi< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#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() @@ -83,11 +76,9 @@ public static IHttpClientBuilder AddHttpApi< /// 接口类型 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType) + public static IHttpClientBuilder AddHttpApi( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType) { if (httpApiType == null) { @@ -110,11 +101,10 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, /// 配置选项 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, Action configureOptions) + public static IHttpClientBuilder AddHttpApi( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + Action configureOptions) { return services .AddHttpApi(httpApiType) @@ -129,11 +119,10 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, /// 配置选项 /// /// - public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, Action configureOptions) + public static IHttpClientBuilder AddHttpApi( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + Action configureOptions) { return services .AddHttpApi(httpApiType) @@ -146,11 +135,7 @@ public static IHttpClientBuilder AddHttpApi(this IServiceCollection services, /// 表示THttpApi提供者 /// /// - private class HttpApiProvider< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> + private class HttpApiProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> { private readonly IHttpClientFactory httpClientFactory; private readonly IOptionsMonitor httpApiOptionsMonitor; @@ -203,15 +188,9 @@ private abstract class HttpApiAdder /// /// 接口类型 /// -#if NET5_0_OR_GREATER [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(HttpApiAdderOf<>))] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] -#endif - public static HttpApiAdder Create( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType) + public static HttpApiAdder Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType) { var adderType = typeof(HttpApiAdderOf<>).MakeGenericType(httpApiType); return adderType.CreateInstance(); @@ -221,11 +200,8 @@ public static HttpApiAdder Create( /// 表示HttpApi服务添加者 /// /// - private class HttpApiAdderOf< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> : HttpApiAdder where THttpApi : class + private class HttpApiAdderOf<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : HttpApiAdder where THttpApi : class { /// /// 添加HttpApi代理类到服务 diff --git a/WebApiClientCore/HttpApi.cs b/WebApiClientCore/HttpApi.cs index af39d83a..54c65d33 100644 --- a/WebApiClientCore/HttpApi.cs +++ b/WebApiClientCore/HttpApi.cs @@ -91,15 +91,9 @@ private static void GetName(Type type, ref ValueStringBuilder builder) /// 接口类型 /// /// - /// -#if NET5_0_OR_GREATER + /// [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "已使用DynamicallyAccessedMembers.All关联接口的父接口成员")] -#endif - public static MethodInfo[] FindApiMethods( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType) + public static MethodInfo[] FindApiMethods([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType) { if (httpApiType.IsInterface == false) { diff --git a/WebApiClientCore/HttpContents/FormContent.cs b/WebApiClientCore/HttpContents/FormContent.cs index d7ef2fec..bcb159d5 100644 --- a/WebApiClientCore/HttpContents/FormContent.cs +++ b/WebApiClientCore/HttpContents/FormContent.cs @@ -57,10 +57,8 @@ public FormContent(IEnumerable keyValues) /// /// 模型对象值 /// 序列化选项 -#if NET8_0_OR_GREATER [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.")] -#endif public FormContent(object? value, KeyValueSerializerOptions? options) { if (value != null) diff --git a/WebApiClientCore/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index 2023647f..8f3e86dd 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -29,10 +29,8 @@ public JsonContent() /// /// 对象值 /// json序列化选项 -#if NET8_0_OR_GREATER [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.")] -#endif public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) : this(value, jsonSerializerOptions, null) { @@ -44,10 +42,8 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) /// 对象值 /// json序列化选项 /// 编码 -#if NET8_0_OR_GREATER [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.")] -#endif public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions, Encoding? encoding) : base(MediaType) { diff --git a/WebApiClientCore/HttpContents/JsonPatchContent.cs b/WebApiClientCore/HttpContents/JsonPatchContent.cs index 9cff4f8c..c2cfb567 100644 --- a/WebApiClientCore/HttpContents/JsonPatchContent.cs +++ b/WebApiClientCore/HttpContents/JsonPatchContent.cs @@ -28,10 +28,8 @@ public JsonPatchContent() /// /// patch操作项 /// json序列化选项 -#if NET8_0_OR_GREATER [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.")] -#endif public JsonPatchContent(IEnumerable operations, JsonSerializerOptions? jsonSerializerOptions) : base(MediaType) { diff --git a/WebApiClientCore/Implementations/DataValidator.cs b/WebApiClientCore/Implementations/DataValidator.cs index 4834a70a..a63f1f8c 100644 --- a/WebApiClientCore/Implementations/DataValidator.cs +++ b/WebApiClientCore/Implementations/DataValidator.cs @@ -25,9 +25,7 @@ static class DataValidator /// 参数值 /// 是否验证属性值 /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许instance属性被裁剪")] -#endif public static void ValidateParameter(ApiParameterDescriptor parameter, object? parameterValue, bool validateProperty) { var name = parameter.Name; @@ -48,9 +46,7 @@ public static void ValidateParameter(ApiParameterDescriptor parameter, object? p /// /// 结果值 /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许instance属性被裁剪")] -#endif public static void ValidateReturnValue(object? value) { if (value != null && IsNeedValidateProperty(value) == true) @@ -65,9 +61,7 @@ public static void ValidateReturnValue(object? value) /// /// 实例 /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "允许instance属性被裁剪")] -#endif private static bool IsNeedValidateProperty(object instance) { var type = instance.GetType(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs index 5f691160..d9033bfc 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs @@ -18,9 +18,8 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// 获取所在接口类型 /// 这个值不一定是声明方法的接口类型 /// -#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif public override Type InterfaceType { get; protected set; } /// @@ -68,10 +67,8 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// 请求Api描述 /// for test only /// - /// 接口的方法 -#if NET5_0_OR_GREATER + /// 接口的方法 [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "允许DeclaringType被裁剪")] -#endif internal DefaultApiActionDescriptor(MethodInfo method) : this(method, method.DeclaringType!) { @@ -82,11 +79,9 @@ internal DefaultApiActionDescriptor(MethodInfo method) /// /// 接口的方法 /// 接口类型 - public DefaultApiActionDescriptor(MethodInfo method, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type interfaceType) + public DefaultApiActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { var methodAttributes = method.GetCustomAttributes().ToArray(); var interfaceAttributes = interfaceType.GetInterfaceCustomAttributes(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs index 922b2848..aad7d307 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionDescriptorProvider.cs @@ -14,11 +14,9 @@ public class DefaultApiActionDescriptorProvider : IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - public virtual ApiActionDescriptor CreateActionDescriptor(MethodInfo method, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type interfaceType) + public virtual ApiActionDescriptor CreateActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { return new DefaultApiActionDescriptor(method, interfaceType); } diff --git a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs index db909550..56ca3ed3 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs @@ -30,11 +30,9 @@ public ApiActionInvoker CreateActionInvoker(ApiActionDescriptor actionDescriptor /// 创建DefaultApiActionInvoker类型或其子类型的实例 /// /// Action描述 - /// -#if NET5_0_OR_GREATER + /// [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(DefaultApiActionInvoker<>))] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] -#endif protected virtual ApiActionInvoker CreateDefaultActionInvoker(ApiActionDescriptor actionDescriptor) { var resultType = actionDescriptor.Return.DataType.Type; diff --git a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs index 6408d6ce..e71652f9 100644 --- a/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiReturnDescriptor.cs @@ -35,9 +35,7 @@ public class DefaultApiReturnDescriptor : ApiReturnDescriptor /// for test only /// /// 方法信息 -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "允许DeclaringType被裁剪")] -#endif internal DefaultApiReturnDescriptor(MethodInfo method) : this(method, method.DeclaringType!) { @@ -48,11 +46,9 @@ internal DefaultApiReturnDescriptor(MethodInfo method) /// /// 方法信息 /// 接口类型 - public DefaultApiReturnDescriptor(MethodInfo method, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type interfaceType) + public DefaultApiReturnDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) : this(method.ReturnType, method.GetCustomAttributes(), interfaceType.GetInterfaceCustomAttributes()) { } diff --git a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs index 37c9380a..729de8b5 100644 --- a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs @@ -9,11 +9,8 @@ namespace WebApiClientCore.Implementations /// 不支持则回退使用EmitHttpApiActivator /// /// - public class DefaultHttpApiActivator< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> : IHttpApiActivator + public class DefaultHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { private readonly IHttpApiActivator httpApiActivator; @@ -24,9 +21,7 @@ public class DefaultHttpApiActivator< /// /// /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] -#endif public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { this.httpApiActivator = SourceGeneratorHttpApiActivator.IsSupported diff --git a/WebApiClientCore/Implementations/HttpApiActivator.cs b/WebApiClientCore/Implementations/HttpApiActivator.cs index 55243d9d..ed10cefa 100644 --- a/WebApiClientCore/Implementations/HttpApiActivator.cs +++ b/WebApiClientCore/Implementations/HttpApiActivator.cs @@ -10,11 +10,8 @@ namespace WebApiClientCore.Implementations /// /// [Obsolete("该类型存在构造器调用虚方法的设计失误,不建议再使用", error: false)] - public abstract class HttpApiActivator< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> : IHttpApiActivator + public abstract class HttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { /// /// 接口的所有方法执行器 diff --git a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs index 2b94eb9a..35c761e1 100644 --- a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs @@ -12,11 +12,8 @@ namespace WebApiClientCore.Implementations /// 运行时使用ILEmit动态创建THttpApi的代理类和代理类实例 /// /// - public sealed class ILEmitHttpApiActivator< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> : IHttpApiActivator + public sealed class ILEmitHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { private readonly ApiActionInvoker[] actionInvokers; private readonly Func activator; @@ -28,12 +25,8 @@ public sealed class ILEmitHttpApiActivator< /// /// /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "proxyType是运行时生成的")] -#endif -#if NET8_0_OR_GREATER [RequiresDynamicCode("Calls System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)")] -#endif public ILEmitHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var apiMethods = HttpApi.FindApiMethods(typeof(THttpApi)); @@ -78,9 +71,7 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// /// /// -#if NET8_0_OR_GREATER [RequiresDynamicCode("Calls System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)")] -#endif private static Type BuildProxyType(MethodInfo[] apiMethods) { // 接口的实现在动态程序集里,所以接口必须为public修饰才可以创建代理类并实现此接口 diff --git a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs index 0e5352f0..125a9989 100644 --- a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs +++ b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs @@ -24,11 +24,9 @@ public class JsonFirstApiActionDescriptor : DefaultApiActionDescriptor /// /// /// - public JsonFirstApiActionDescriptor(MethodInfo method, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type interfaceType) + public JsonFirstApiActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) : base(method, interfaceType) { var defineGetHead = this.Attributes.Any(a => this.IsGetHeadAttribute(a)); diff --git a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs index 13942b45..1980394c 100644 --- a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs +++ b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptorProvider.cs @@ -15,11 +15,9 @@ public class JsonFirstApiActionDescriptorProvider : IApiActionDescriptorProvider /// /// 接口的方法 /// 接口类型 - public ApiActionDescriptor CreateActionDescriptor(MethodInfo method, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type interfaceType) + public ApiActionDescriptor CreateActionDescriptor( + MethodInfo method, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { return new JsonFirstApiActionDescriptor(method, interfaceType); } diff --git a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs index 4c44ca5c..775a1461 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -14,11 +14,8 @@ namespace WebApiClientCore.Implementations /// 通过查找类型代理类型创建实例 /// /// - public sealed class SourceGeneratorHttpApiActivator< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - THttpApi> : IHttpApiActivator + public sealed class SourceGeneratorHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi> + : IHttpApiActivator { private readonly ApiActionInvoker[] actionInvokers; private readonly Func activator; @@ -37,9 +34,7 @@ public sealed class SourceGeneratorHttpApiActivator< /// /// /// -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "类型proxyClassType已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] -#endif public SourceGeneratorHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var httpApiType = typeof(THttpApi); @@ -75,14 +70,8 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) /// 接口的实现类型 /// private static IEnumerable FindApiMethods( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type httpApiType, -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type proxyClassType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type httpApiType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type proxyClassType) { var apiMethods = HttpApi.FindApiMethods(httpApiType) .Select(item => new MethodFeature(item, isProxyMethod: false)) diff --git a/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs index 0b546a5a..37e71adf 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorProxyClassFinder.cs @@ -37,10 +37,8 @@ static class SourceGeneratorProxyClassFinder return null; } -#if NET5_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "类型已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075", Justification = "类型已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] -#endif private static void AnalyzeAssembly(Assembly assembly) { var httpApiProxyClass = assembly.GetType(HttpApiProxyClassTypeName); diff --git a/WebApiClientCore/Internals/LambdaUtil.cs b/WebApiClientCore/Internals/LambdaUtil.cs index 7c15829e..b981a36d 100644 --- a/WebApiClientCore/Internals/LambdaUtil.cs +++ b/WebApiClientCore/Internals/LambdaUtil.cs @@ -88,7 +88,7 @@ public static Func CreateGetFunc(P } return Expression.Lambda>(bodyProperty, paramInstance).Compile(); - } + } /// /// 创建字段的获取委托 @@ -132,10 +132,7 @@ public static Func CreateGetFunc(FieldIn /// 类型 /// public static Func CreateCtorFunc( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - Type type) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { return CreateCtorFunc>(type, Array.Empty()); } @@ -150,10 +147,7 @@ public static Func CreateCtorFunc( /// /// public static Func CreateCtorFunc( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - Type type) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { var args = new Type[] { typeof(TArg1) }; return CreateCtorFunc>(type, args); @@ -170,10 +164,7 @@ public static Func CreateCtorFunc( /// /// public static Func CreateCtorFunc( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - Type type) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { var args = new Type[] { typeof(TArg1), typeof(TArg2) }; return CreateCtorFunc>(type, args); @@ -188,11 +179,7 @@ public static Func CreateCtorFunc( /// /// /// - public static Func CreateCtorFunc( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - 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); @@ -208,10 +195,8 @@ public static Func CreateCtorFunc /// private static TFunc CreateCtorFunc( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - Type type, Type[] args) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, + Type[] args) { if (type == null) { diff --git a/WebApiClientCore/Parameters/JsonPatchDocument.cs b/WebApiClientCore/Parameters/JsonPatchDocument.cs index f44433a2..ceba2432 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -69,10 +69,8 @@ public void Replace(string path, object? value) /// /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public Task OnRequestAsync(ApiParameterContext context) { if (context.HttpContext.RequestMessage.Method != HttpMethod.Patch) diff --git a/WebApiClientCore/Serialization/JsonBufferSerializer.cs b/WebApiClientCore/Serialization/JsonBufferSerializer.cs index 6fff704e..1ffbf60f 100644 --- a/WebApiClientCore/Serialization/JsonBufferSerializer.cs +++ b/WebApiClientCore/Serialization/JsonBufferSerializer.cs @@ -21,10 +21,8 @@ public static class JsonBufferSerializer /// buffer写入器 /// 对象 /// 选项 -#if NET8_0_OR_GREATER [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.")] -#endif public static void Serialize(IBufferWriter bufferWriter, object? obj, JsonSerializerOptions? options) { if (obj == null) diff --git a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs index 0a9b534d..2856d81d 100644 --- a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs +++ b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs @@ -31,11 +31,9 @@ public override bool CanConvert(Type typeToConvert) /// /// /// - /// -#if NET5_0_OR_GREATER + /// [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(Converter<>))] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "类型已使用DynamicDependency来阻止被裁剪")] -#endif public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { return typeof(Converter<>).MakeGenericType(typeToConvert).CreateInstance(); @@ -55,11 +53,9 @@ private class Converter : JsonConverter where TJsonStr /// /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var json = reader.GetString(); @@ -74,10 +70,8 @@ public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, /// /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif 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 ff489302..8844b1ab 100644 --- a/WebApiClientCore/Serialization/KeyValueSerializer.cs +++ b/WebApiClientCore/Serialization/KeyValueSerializer.cs @@ -26,10 +26,8 @@ public static class KeyValueSerializer /// 对象实例 /// 选项 /// -#if NET8_0_OR_GREATER [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.")] -#endif public static IList Serialize(string key, object? obj, KeyValueSerializerOptions? options) { var kvOptions = options ?? defaultOptions; @@ -78,10 +76,8 @@ public static IList Serialize(string key, object? obj, KeyValueSeriali /// 对象类型 /// 选项 /// -#if NET8_0_OR_GREATER [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.")] -#endif private static IList GetKeyValueList(string key, object obj, Type objType, KeyValueSerializerOptions options) { var jsonOptions = options.GetJsonSerializerOptions(); diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index 1dedbe81..d7208a58 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -20,9 +20,7 @@ public static class XmlSerializer /// 对象 /// 配置选项 /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] -#endif public static string? Serialize(object? obj, XmlWriterSettings? options) { if (obj == null) @@ -45,9 +43,7 @@ public static class XmlSerializer /// 对象类型 /// 配置选项 /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] -#endif public static object? Deserialize(string? xml, Type objType, XmlReaderSettings? options) { if (objType == null) From 22bba9247c998968016cdc462ad48f509711bcea Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sat, 15 Jun 2024 17:52:59 +0800 Subject: [PATCH 050/108] =?UTF-8?q?=E6=B6=88=E9=99=A4aot=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppAot/AppHostedService.cs | 9 +++---- AppAot/ICloudflareApi.cs | 5 +--- .../RequiresDynamicCodeAttribute.cs | 25 +++++++++++++++++++ .../RequiresUnreferencedCodeAttribute.cs | 22 ++++++++++++++++ .../HttpApiOptions.cs | 8 +++++- .../JsonConverters/JsonCompatibleConverter.cs | 15 +++++++++-- .../RequiresDynamicCodeAttribute.cs | 25 +++++++++++++++++++ .../RequiresUnreferencedCodeAttribute.cs | 22 ++++++++++++++++ .../HttpApiSettings.cs | 8 +++--- .../DefaultHttpApiActivator.cs | 19 +++++++++++--- 10 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 WebApiClientCore.Abstractions/CodeAnalysis/RequiresDynamicCodeAttribute.cs create mode 100644 WebApiClientCore.Abstractions/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs create mode 100644 WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresDynamicCodeAttribute.cs create mode 100644 WebApiClientCore.Extensions.OAuths/CodeAnalysis/RequiresUnreferencedCodeAttribute.cs 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..407465e8 100644 --- a/AppAot/ICloudflareApi.cs +++ b/AppAot/ICloudflareApi.cs @@ -9,9 +9,6 @@ namespace AppAot public interface ICloudflareApi { [HttpGet("/page-data/app-data.json")] - Task GetAppDataAsync(); - - [HttpGet("/page-data/app-data.json")] - ITask GetAppData2Async(); + Task GetAppDataAsync(); } } 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/HttpApiOptions.cs b/WebApiClientCore.Abstractions/HttpApiOptions.cs index 196e3c8f..22d4ef3b 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; @@ -96,10 +98,14 @@ 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; } 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.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.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/Implementations/DefaultHttpApiActivator.cs b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs index 729de8b5..ac777da4 100644 --- a/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/DefaultHttpApiActivator.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using WebApiClientCore.Exceptions; namespace WebApiClientCore.Implementations { @@ -21,12 +23,21 @@ public class DefaultHttpApiActivator<[DynamicallyAccessedMembers(DynamicallyAcce /// /// /// - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "ILEmitHttpApiActivator使用之前已经使用RuntimeFeature.IsDynamicCodeCompiled来判断")] public DefaultHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { - this.httpApiActivator = SourceGeneratorHttpApiActivator.IsSupported - ? new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider) - : new ILEmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); + if (SourceGeneratorHttpApiActivator.IsSupported) + { + this.httpApiActivator = new SourceGeneratorHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); + } + else if (RuntimeFeature.IsDynamicCodeCompiled) + { + this.httpApiActivator = new ILEmitHttpApiActivator(apiActionDescriptorProvider, actionInvokerProvider); + } + else + { + throw new ProxyTypeCreateException(typeof(HttpApi)); + } } /// From fa9187e8b1bd7e3232be9892db36c93bbec6409c Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sat, 15 Jun 2024 20:44:50 +0800 Subject: [PATCH 051/108] =?UTF-8?q?=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- .../StringReplaces/Benchmark.cs | 2 +- .../BuildinExtensions/StringExtensionsTest.cs | 10 ++--- .../ApiParameterContextExtensions.cs | 4 +- .../ApiRequestContextExtensions.cs | 2 +- .../ActionAttributes/FormDataTextAttribute.cs | 4 +- .../ActionAttributes/FormFieldAttribute.cs | 4 +- .../ActionAttributes/HttpHostAttribute.cs | 8 ++-- .../ActionAttributes/HttpMethodAttribute.cs | 2 +- .../CacheAttributes/CacheAttribute.cs | 2 +- .../LoggingFilterAttribute.cs | 4 +- .../FormContentAttribute.cs | 6 +-- .../FormDataContentAttribute.cs | 6 +-- .../FormDataTextAttribute.cs | 4 +- .../ParameterAttributes/FormFieldAttribute.cs | 4 +- .../ParameterAttributes/HeadersAttribute.cs | 2 +- .../HttpContentAttribute.cs | 10 ++++- .../JsonContentAttribute.cs | 4 +- .../JsonFormDataTextAttribute.cs | 2 +- .../JsonFormFieldAttribute.cs | 2 +- .../ParameterAttributes/PathQueryAttribute.cs | 8 ++-- .../RawFormContentAttribute.cs | 2 +- .../RawJsonContentAttribute.cs | 4 +- .../RawStringContentAttribute.cs | 2 +- .../RawXmlContentAttribute.cs | 4 +- .../ParameterAttributes/UriAttribute.cs | 2 +- .../XmlContentAttribute.cs | 4 +- .../ReturnAttributes/ApiReturnAttribute.cs | 28 ++++++------- .../ReturnAttributes/JsonReturnAttribute.cs | 6 +-- .../ReturnAttributes/NoneReturnAttribute.cs | 3 +- .../ReturnAttributes/RawReturnAttribute.cs | 8 ++-- .../ReturnAttributes/XmlReturnAttribute.cs | 4 +- .../BuildinExtensions/CollectionExtensions.cs | 6 +-- .../BuildinExtensions/EncodingExtensions.cs | 2 +- .../HttpRequestHeaderExtensions.cs | 2 +- .../BuildinExtensions/StringExtensions.cs | 6 +-- .../BuildinExtensions/TypeExtensions.cs | 6 +-- .../NamedHttpApiExtensions.cs | 4 +- .../HttpContents/BufferContent.cs | 2 +- WebApiClientCore/HttpContents/FormContent.cs | 12 +++--- .../HttpContents/FormDataContent.cs | 14 +++---- .../HttpContents/FormDataFileContent.cs | 6 +-- .../HttpContents/FormDataTextContent.cs | 6 +-- .../ICustomHttpContentConvertable.cs | 2 +- WebApiClientCore/HttpContents/JsonContent.cs | 6 +-- .../HttpContents/JsonPatchContent.cs | 2 +- WebApiClientCore/HttpContents/XmlContent.cs | 2 +- .../AuthorizationHandler.cs | 4 +- .../CookieAuthorizationHandler.cs | 2 +- WebApiClientCore/HttpPath.cs | 4 +- .../Implementations/ApiRequestSender.cs | 14 +++---- .../Implementations/DataValidator.cs | 6 +-- .../DefaultApiActionInvokerProvider.cs | 4 +- .../DefaultApiDataTypeDescriptor.cs | 2 +- .../DefaultApiParameterDescriptor.cs | 6 +-- .../Implementations/DefaultDataCollection.cs | 4 +- .../HttpApiRequestMessageImpl.cs | 26 ++++++------ .../Implementations/ILEmitHttpApiActivator.cs | 6 +-- .../JsonFirstApiActionDescriptor.cs | 2 +- .../Implementations/MultiplableComparer.cs | 2 +- .../SourceGeneratorHttpApiActivator.cs | 4 +- ...{AcitonRetryTask.cs => ActionRetryTask.cs} | 16 +++---- WebApiClientCore/Internals/HttpUtil.cs | 42 +++++++++---------- WebApiClientCore/Internals/LambdaUtil.cs | 2 +- WebApiClientCore/Internals/UriValue.cs | 18 ++++---- .../Internals/ValueStringBuilder.cs | 6 +-- WebApiClientCore/JsonString.cs | 2 +- WebApiClientCore/Parameters/FormDataFile.cs | 14 +++---- .../Parameters/JsonPatchDocument.cs | 4 +- .../Serialization/JsonBufferSerializer.cs | 2 +- .../JsonConverters/JsonStringTypeConverter.cs | 4 +- .../Serialization/KeyValueSerializer.cs | 12 +++--- .../Serialization/XmlSerializer.cs | 4 +- .../System.Net.Http/HttpContentExtensions.cs | 4 +- .../System.Net.Http/HttpProgress.cs | 2 +- WebApiClientCore/TaskExtenstions.cs | 10 ++--- 76 files changed, 226 insertions(+), 239 deletions(-) rename WebApiClientCore/Implementations/Tasks/{AcitonRetryTask.cs => ActionRetryTask.cs} (93%) diff --git a/Directory.Build.props b/Directory.Build.props index a059d206..2f917f9b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ 2.1.2 Copyright © laojiu 2017-2024 - IDE0057;IDE0290 + IDE0290;NETSDK1138 diff --git a/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs b/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs index e5657c32..5fb83c99 100644 --- a/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs @@ -26,7 +26,7 @@ public void ReplaceByRegexStatic() [Benchmark] public void ReplaceByCutomSpan() { - str.RepaceIgnoreCase(pattern, replacement, out var _); + str.ReplaceIgnoreCase(pattern, replacement, out var _); } } } 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/ApiParameterContextExtensions.cs b/WebApiClientCore/ApiParameterContextExtensions.cs index 02224825..07827f59 100644 --- a/WebApiClientCore/ApiParameterContextExtensions.cs +++ b/WebApiClientCore/ApiParameterContextExtensions.cs @@ -13,7 +13,7 @@ namespace WebApiClientCore public static class ApiParameterContextExtensions { /// - /// 序列化参数值为utf8编码的Json + /// 序列化参数值为 utf8 编码的 json /// /// /// @@ -49,7 +49,7 @@ public static byte[] SerializeToJson(this ApiParameterContext context, Encoding } /// - /// 序列化参数值为utf8编码的Json + /// 序列化参数值为 utf8 编码的 json /// /// /// buffer写入器 diff --git a/WebApiClientCore/ApiRequestContextExtensions.cs b/WebApiClientCore/ApiRequestContextExtensions.cs index 6fd2f877..eb6167cd 100644 --- a/WebApiClientCore/ApiRequestContextExtensions.cs +++ b/WebApiClientCore/ApiRequestContextExtensions.cs @@ -99,7 +99,7 @@ internal static HttpCompletionOption GetCompletionOption(this ApiRequestContext } /// - /// 获取指向api方法名的日志 + /// 获取指向 api 方法名的日志 /// /// /// 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 58021f7e..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 表单 /// /// 字段名称 /// 字段的值 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/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index d70eb8ea..19b23498 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -104,8 +104,8 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) 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); } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs index 6f8c900c..6f8be3b7 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormContentAttribute.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用KeyValueSerializer序列化参数值得到的键值对作为x-www-form-urlencoded表单 + /// 使用KeyValueSerializer序列化参数值得到的键值对作为 x-www-form-urlencoded 表单 /// public class FormContentAttribute : HttpContentAttribute, ICollectionFormatable { @@ -15,7 +15,7 @@ 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 = "")] @@ -27,7 +27,7 @@ protected override async Task SetHttpContentAsync(ApiParameterContext context) } /// - /// 序列化参数为keyValue + /// 序列化参数为 keyValue 集合 /// /// 上下文 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs index e608fb65..713bdb07 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/FormDataContentAttribute.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用KeyValueSerializer序列化参数值得到的键值作为multipart/form-data表单 + /// 使用KeyValueSerializer序列化参数值得到的键值作为 multipart/form-data 表单 /// public class FormDataContentAttribute : HttpContentAttribute, ICollectionFormatable { @@ -15,7 +15,7 @@ public class FormDataContentAttribute : HttpContentAttribute, ICollectionFormata public CollectionFormat CollectionFormat { get; set; } = CollectionFormat.Multi; /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// @@ -27,7 +27,7 @@ protected override Task SetHttpContentAsync(ApiParameterContext context) } /// - /// 序列化参数为keyValue + /// 序列化参数为 keyValue 集合 /// /// 上下文 /// 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/HeadersAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs index 4fadb12e..09f6d5e0 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HeadersAttribute.cs @@ -12,7 +12,7 @@ public class HeadersAttribute : ApiParameterAttribute { /// /// 获取或设置是否将请求名的_转换为- - /// 默认为true + /// 默认为 true /// public bool UnderlineToMinus { get; set; } = true; diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs index c2476d4b..deb59788 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请求之前 /// @@ -22,14 +25,17 @@ public sealed override async Task OnRequestAsync(ApiParameterContext context) if (method == HttpMethod.Get || method == HttpMethod.Head) { var logger = context.GetLogger(); - logger?.LogWarning(Resx.gethead_Content_Warning.Format(method)); + 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 6f4fe2a2..83cda5bb 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用JsonSerializer序列化参数值得到的json文本作为application/json请求 + /// 使用JsonSerializer序列化参数值得到的 json 文本作为 application/json 请求 /// 每个Api只能注明于其中的一个参数 /// public class JsonContentAttribute : HttpContentAttribute, ICharSetable @@ -28,7 +28,7 @@ public string CharSet } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs index be0c7dc7..85048061 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示参数值序列化为Json并作为multipart/form-data表单的一个文本项 + /// 表示参数值序列化为 json 并作为 multipart/form-data 表单的一个文本项 /// public class JsonFormDataTextAttribute : ApiParameterAttribute { diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs index aec5e366..5792a01d 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs @@ -5,7 +5,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示参数值序列化为Json并作为x-www-form-urlencoded的字段 + /// 表示参数值序列化为 json 并作为 x-www-form-urlencoded 的字段 /// public class JsonFormFieldAttribute : ApiParameterAttribute { diff --git a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs index ae766830..c7fb01cf 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/PathQueryAttribute.cs @@ -8,7 +8,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用KeyValueSerializer序列化参数值得到的键值对作为url路径参数或query参数的特性 + /// 使用KeyValueSerializer序列化参数值得到的键值对作为 url 路径参数或 query 参数的特性 /// public class PathQueryAttribute : ApiParameterAttribute, ICollectionFormatable { @@ -39,7 +39,7 @@ public override Task OnRequestAsync(ApiParameterContext context) } /// - /// 序列化参数为keyValue + /// 序列化参数为 keyValue 集合 /// /// 上下文 /// @@ -51,9 +51,9 @@ protected virtual IEnumerable SerializeToKeyValues(ApiParameterContext } /// - /// 创建新的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 abe560bf..14ff92fd 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Attributes { /// - /// 使用XmlSerializer序列化参数值得到的xml文本作为application/xml请求 + /// 使用XmlSerializer序列化参数值得到的 xml 文本作为 application/xml 请求 /// public class XmlContentAttribute : HttpContentAttribute, ICharSetable { @@ -27,7 +27,7 @@ public string CharSet } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// 上下文 /// diff --git a/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/ApiReturnAttribute.cs index e38fa3b5..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一致 - /// 默认为false + /// 获取或设置是否确保响应的ContentType与指定的 Accept-ContentType 一致 + /// 默认为 false /// 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 f89bbfd8..3222b158 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示json内容的结果特性 + /// 表示 json 内容的结果特性 /// public class JsonReturnAttribute : ApiReturnAttribute { @@ -17,7 +17,7 @@ public class JsonReturnAttribute : ApiReturnAttribute private static readonly string textJson = "text/json"; /// - /// 问题描述json + /// 问题描述 json /// private static readonly string problemJson = "application/problem+json"; @@ -40,7 +40,7 @@ public JsonReturnAttribute(double acceptQuality) /// /// 指示响应的ContentType与AcceptContentType是否匹配 - /// 返回false则调用下一个ApiReturnAttribute来处理响应结果 + /// 返回 false 则调用下一个ApiReturnAttribute来处理响应结果 /// /// 响应的ContentType /// 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..29230552 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; diff --git a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs index b5dfacbf..49aa9aa8 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Attributes { /// - /// 表示xml内容的结果特性 + /// 表示 xml 内容的结果特性 /// public class XmlReturnAttribute : ApiReturnAttribute { @@ -35,7 +35,7 @@ public XmlReturnAttribute(double acceptQuality) /// /// 指示响应的ContentType与AcceptContentType是否匹配 - /// 返回false则调用下一个ApiReturnAttribute来处理响应结果 + /// 返回 false 则调用下一个ApiReturnAttribute来处理响应结果 /// /// 响应的ContentType /// 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..e7ef3fd1 100644 --- a/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs @@ -22,7 +22,7 @@ public static void Convert(this Encoding srcEncoding, Encoding dstEncoding, Read 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); + var chars = charArray == null ? stackalloc char[charCount] : charArray.AsSpan()[..charCount]; try { diff --git a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs index f4773471..a96fc904 100644 --- a/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/HttpRequestHeaderExtensions.cs @@ -57,7 +57,7 @@ static class HttpRequestHeaderExtensions }; /// - /// 转换为header名 + /// 转换为 header 名 /// /// 请求头枚举 /// 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 dc8c0864..cb2f4831 100644 --- a/WebApiClientCore/BuildinExtensions/TypeExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/TypeExtensions.cs @@ -68,11 +68,7 @@ public static T CreateInstance( 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; } /// 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/HttpContents/BufferContent.cs b/WebApiClientCore/HttpContents/BufferContent.cs index 8c92e5bf..a50232a8 100644 --- a/WebApiClientCore/HttpContents/BufferContent.cs +++ b/WebApiClientCore/HttpContents/BufferContent.cs @@ -10,7 +10,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示utf8的BufferContent + /// 表示 utf8 的BufferContent /// public class BufferContent : HttpContent, IBufferWriter { diff --git a/WebApiClientCore/HttpContents/FormContent.cs b/WebApiClientCore/HttpContents/FormContent.cs index bcb159d5..422b51de 100644 --- a/WebApiClientCore/HttpContents/FormContent.cs +++ b/WebApiClientCore/HttpContents/FormContent.cs @@ -14,7 +14,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示键值对表单内容 + /// 表示 application/x-www-form-urlencoded 表单内容 /// public class FormContent : HttpContent { @@ -24,7 +24,7 @@ public class FormContent : HttpContent private readonly RecyclableBufferWriter bufferWriter = new(); /// - /// 默认的http编码 + /// 默认的 http 编码 /// private static readonly Encoding httpEncoding = Encoding.GetEncoding(28591); @@ -35,7 +35,7 @@ public class FormContent : HttpContent /// - /// 键值对表单内容 + /// application/x-www-form-urlencoded 表单内容 /// public FormContent() { @@ -43,7 +43,7 @@ public FormContent() } /// - /// 键值对表单内容 + /// application/x-www-form-urlencoded 表单内容 /// /// 键值对 public FormContent(IEnumerable keyValues) @@ -53,7 +53,7 @@ public FormContent(IEnumerable keyValues) } /// - /// 键值对表单内容 + /// application/x-www-form-urlencoded 表单内容 /// /// 模型对象值 /// 序列化选项 @@ -187,7 +187,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 8f3e86dd..16e9a45e 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示uft8的json内容 + /// 表示 uft8 的 json 内容 /// public class JsonContent : BufferContent { @@ -17,7 +17,7 @@ public class JsonContent : BufferContent public static string MediaType => "application/json"; /// - /// uft8的json内容 + /// uft8 的 json 内容 /// public JsonContent() : base(MediaType) @@ -25,7 +25,7 @@ public JsonContent() } /// - /// uft8的json内容 + /// uft8 的 json 内容 /// /// 对象值 /// json序列化选项 diff --git a/WebApiClientCore/HttpContents/JsonPatchContent.cs b/WebApiClientCore/HttpContents/JsonPatchContent.cs index c2cfb567..4aa79725 100644 --- a/WebApiClientCore/HttpContents/JsonPatchContent.cs +++ b/WebApiClientCore/HttpContents/JsonPatchContent.cs @@ -6,7 +6,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示utf8的JsonPatch内容 + /// 表示 utf8 的JsonPatch内容 /// public class JsonPatchContent : BufferContent { diff --git a/WebApiClientCore/HttpContents/XmlContent.cs b/WebApiClientCore/HttpContents/XmlContent.cs index 0ba4c35f..2fbac101 100644 --- a/WebApiClientCore/HttpContents/XmlContent.cs +++ b/WebApiClientCore/HttpContents/XmlContent.cs @@ -4,7 +4,7 @@ namespace WebApiClientCore.HttpContents { /// - /// 表示xml内容 + /// 表示 xml 内容 /// public class XmlContent : StringContent { 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/Implementations/ApiRequestSender.cs b/WebApiClientCore/Implementations/ApiRequestSender.cs index f0a26766..4c784073 100644 --- a/WebApiClientCore/Implementations/ApiRequestSender.cs +++ b/WebApiClientCore/Implementations/ApiRequestSender.cs @@ -9,12 +9,12 @@ namespace WebApiClientCore.Implementations { /// - /// 提供http请求 + /// 提供 http 请求 /// static class ApiRequestSender { /// - /// 发送http请求 + /// 发送 http 请求 /// /// /// @@ -38,14 +38,14 @@ public static async Task SendAsync(ApiRequestContext context } /// - /// 发送http请求 + /// 发送 http 请求 /// /// /// /// private static async Task SendCoreAsync(ApiRequestContext context) { - 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; @@ -69,7 +69,7 @@ private static async Task SendCoreAsync(ApiRequestContext context) /// /// /// - private static async Task GetCaheAsync(this ApiRequestContext context) + private static async Task GetCacheAsync(this ApiRequestContext context) { var attribute = context.ActionDescriptor.CacheAttribute; if (attribute == null) @@ -184,12 +184,12 @@ public ActionCache(string key, HttpResponseMessage? value) private readonly struct CancellationTokenLinker : IDisposable { /// - /// 链接产生的tokenSource + /// 链接产生的 tokenSource /// private readonly CancellationTokenSource? tokenSource; /// - /// 获取token + /// 获取 token /// public CancellationToken Token { get; } diff --git a/WebApiClientCore/Implementations/DataValidator.cs b/WebApiClientCore/Implementations/DataValidator.cs index a63f1f8c..b453f81d 100644 --- a/WebApiClientCore/Implementations/DataValidator.cs +++ b/WebApiClientCore/Implementations/DataValidator.cs @@ -25,7 +25,7 @@ static class DataValidator /// 参数值 /// 是否验证属性值 /// - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许instance属性被裁剪")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许 parameterValue 属性被裁剪")] public static void ValidateParameter(ApiParameterDescriptor parameter, object? parameterValue, bool validateProperty) { var name = parameter.Name; @@ -46,7 +46,7 @@ public static void ValidateParameter(ApiParameterDescriptor parameter, object? p /// /// 结果值 /// - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许instance属性被裁剪")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "允许 value 属性被裁剪")] public static void ValidateReturnValue(object? value) { if (value != null && IsNeedValidateProperty(value) == true) @@ -61,7 +61,7 @@ public static void ValidateReturnValue(object? value) /// /// 实例 /// - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "允许instance属性被裁剪")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "允许 instance 属性被裁剪")] private static bool IsNeedValidateProperty(object instance) { var type = instance.GetType(); diff --git a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs index 56ca3ed3..d324f555 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionInvokerProvider.cs @@ -18,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; 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/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/HttpApiRequestMessageImpl.cs b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs index 1826277b..20e30800 100644 --- a/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs +++ b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs @@ -13,7 +13,7 @@ namespace WebApiClientCore.Implementations { /// - /// 表示httpApi的请求消息 + /// 表示HttpApi的请求消息 /// sealed class HttpApiRequestMessageImpl : HttpApiRequestMessage { @@ -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,14 @@ 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 path[index..].ToString(); } /// @@ -157,7 +157,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 +173,7 @@ public override async Task AddFormFieldAsync(IEnumerable keyValues) /// /// 添加文本内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 键值对 /// @@ -198,7 +198,7 @@ public override void AddFormDataText(IEnumerable keyValues) /// /// 添加文件内容到已有的Content - /// 要求content-type为multipart/form-data + /// 要求 content-type 为 multipart/form-data /// /// 文件流 /// 名称 @@ -220,7 +220,7 @@ public override void AddFormDataFile(Stream stream, string name, string? fileNam } /// - /// 确保前后的mediaType一致 + /// 确保前后的 mediaType 一致 /// /// 新的MediaType /// diff --git a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs index 35c761e1..3b709187 100644 --- a/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/ILEmitHttpApiActivator.cs @@ -74,7 +74,7 @@ public THttpApi CreateInstance(IHttpApiInterceptor apiInterceptor) [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) { @@ -196,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 125a9989..4d02f02a 100644 --- a/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs +++ b/WebApiClientCore/Implementations/JsonFirstApiActionDescriptor.cs @@ -70,7 +70,7 @@ protected virtual bool IsGetHeadAttribute(IApiActionAttribute apiActionAttribute /// 是否为简单类型 /// 这些类型缺省特性时仍然使用PathQueryAttribute /// - /// 真实类型,非nullable + /// 真实类型,非 nullable /// protected virtual bool IsSimpleType(Type realType) { 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 775a1461..953b3b2d 100644 --- a/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs +++ b/WebApiClientCore/Implementations/SourceGeneratorHttpApiActivator.cs @@ -34,7 +34,7 @@ public sealed class SourceGeneratorHttpApiActivator<[DynamicallyAccessedMembers( /// /// /// - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "类型proxyClassType已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "类型 proxyClassType 已使用ModuleInitializer和DynamicDependency来阻止被裁剪")] public SourceGeneratorHttpApiActivator(IApiActionDescriptorProvider apiActionDescriptorProvider, IApiActionInvokerProvider actionInvokerProvider) { var httpApiType = typeof(THttpApi); @@ -88,7 +88,7 @@ private static IEnumerable FindApiMethods( throw new ProxyTypeException(httpApiType, message); } - // 按照Index特征对apiMethods进行排序 + // 按照 Index 特征对 apiMethods 进行排序 return from a in apiMethods join c in classMethods on a equals c 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 b981a36d..002144b6 100644 --- a/WebApiClientCore/Internals/LambdaUtil.cs +++ b/WebApiClientCore/Internals/LambdaUtil.cs @@ -69,7 +69,7 @@ public static Func CreateGetFunc(P var declaringType = property.DeclaringType; if (declaringType == null) { - throw new ArgumentNullException(nameof(property)); + throw new ArgumentException("DeclaringType can not be null", nameof(property)); } // (TDeclaring instance) => (propertyType)((declaringType)instance).propertyName 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 bf112773..6eaa64a7 100644 --- a/WebApiClientCore/Internals/ValueStringBuilder.cs +++ b/WebApiClientCore/Internals/ValueStringBuilder.cs @@ -24,7 +24,7 @@ public ValueStringBuilder(Span buffer) } /// - /// 添加char + /// 添加 char /// /// public void Append(char value) @@ -40,7 +40,7 @@ public void Append(char value) } /// - /// 添加chars + /// 添加 chars /// /// /// @@ -87,7 +87,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/Parameters/FormDataFile.cs b/WebApiClientCore/Parameters/FormDataFile.cs index 659d0d9f..bb614011 100644 --- a/WebApiClientCore/Parameters/FormDataFile.cs +++ b/WebApiClientCore/Parameters/FormDataFile.cs @@ -11,7 +11,7 @@ namespace WebApiClientCore.Parameters { /// - /// 表示form-data的一个文件项 + /// 表示 multipart/form-data 的一个文件项 /// [DebuggerDisplay("FileName = {FileName}")] public class FormDataFile : IApiParameter @@ -39,7 +39,7 @@ public class FormDataFile : IApiParameter public virtual string? EncodedFileName => HttpUtil.UrlEncode(this.FileName, Encoding.UTF8); /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 文件路径 public FormDataFile(string filePath) @@ -48,7 +48,7 @@ public FormDataFile(string filePath) } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 文件信息 public FormDataFile(FileInfo fileInfo) @@ -57,7 +57,7 @@ public FormDataFile(FileInfo fileInfo) } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 数据 /// 文件友好名称 @@ -68,7 +68,7 @@ public FormDataFile(byte[] buffer, string? fileName) : } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// 不支持多线程并发请求 /// 如果多次请求则要求数据流必须支持倒带读取 /// @@ -81,7 +81,7 @@ public FormDataFile(Stream seekableStream, string? fileName) } /// - /// form-data的一个文件项 + /// multipart/form-data的一个文件项 /// /// 数据流的创建委托 /// 文件友好名称 @@ -219,7 +219,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 ceba2432..c1896a14 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -18,7 +18,7 @@ public class JsonPatchDocument : IApiParameter /// /// 操作列表 /// - private readonly List operations = new(); + private readonly List operations = []; /// /// Add操作 @@ -98,7 +98,7 @@ private class DebugView /// 查看的内容 /// [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public List Oprations => this.target.operations; + public List Operations => this.target.operations; /// /// 调试视图 diff --git a/WebApiClientCore/Serialization/JsonBufferSerializer.cs b/WebApiClientCore/Serialization/JsonBufferSerializer.cs index 1ffbf60f..d139dc03 100644 --- a/WebApiClientCore/Serialization/JsonBufferSerializer.cs +++ b/WebApiClientCore/Serialization/JsonBufferSerializer.cs @@ -16,7 +16,7 @@ public static class JsonBufferSerializer private static readonly JsonSerializerOptions defaultOptions = new(); /// - /// 将对象序列化为utf8编码的Json到指定的bufferWriter + /// 将对象序列化为Utf8编码的Json到指定的BufferWriter /// /// buffer写入器 /// 对象 diff --git a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs index 2856d81d..1c51d0f0 100644 --- a/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs +++ b/WebApiClientCore/Serialization/JsonConverters/JsonStringTypeConverter.cs @@ -46,7 +46,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer private class Converter : JsonConverter where TJsonString : IJsonString { /// - /// 将json文本反序列化JsonString的Value的类型 + /// 将Json文本反序列化JsonString的Value的类型 /// 并构建JsonString类型并返回 /// /// @@ -65,7 +65,7 @@ public override TJsonString Read(ref Utf8JsonReader reader, Type typeToConvert, } /// - /// 将JsonString的value序列化文本,并作为json的某字段值 + /// 将JsonString的Value序列化文本,并作为Json的某字段值 /// /// /// diff --git a/WebApiClientCore/Serialization/KeyValueSerializer.cs b/WebApiClientCore/Serialization/KeyValueSerializer.cs index 8844b1ab..d9fe1b28 100644 --- a/WebApiClientCore/Serialization/KeyValueSerializer.cs +++ b/WebApiClientCore/Serialization/KeyValueSerializer.cs @@ -57,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转换,保持原始值 @@ -78,7 +78,7 @@ public static IList Serialize(string key, object? obj, KeyValueSeriali /// [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 IList GetKeyValueList(string key, object obj, Type objType, KeyValueSerializerOptions options) + private static List GetKeyValueList(string key, object obj, Type objType, KeyValueSerializerOptions options) { var jsonOptions = options.GetJsonSerializerOptions(); using var bufferWriter = new RecyclableBufferWriter(); @@ -103,12 +103,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()) @@ -150,13 +150,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/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index d7208a58..ee174f67 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -15,7 +15,7 @@ public static class XmlSerializer private static readonly XmlWriterSettings writerSettings = new(); /// - /// 将对象序列化为xml文本 + /// 将对象序列化为Xml文本 /// /// 对象 /// 配置选项 @@ -37,7 +37,7 @@ public static class XmlSerializer } /// - /// 将xml文本反序列化对象 + /// 将Xml文本反序列化对象 /// /// xml文本内容 /// 对象类型 diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index 3d3dddc6..3c915fdc 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -57,7 +57,7 @@ static HttpContentExtensions() } /// - /// 确保httpContent的内容未被缓存 + /// 确保HttpContent的内容未被缓存 /// 已被缓存则抛出HttpContentBufferedException /// /// @@ -71,7 +71,7 @@ public static void EnsureNotBuffered(this HttpContent httpContent) } /// - /// 读取为二进制数组并转换为utf8编码 + /// 读取为二进制数组并转换为 utf8 编码 /// /// /// 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/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); } } } From 77f9076d9f6be520bc72133ef6f6376039abdc2a Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sat, 15 Jun 2024 21:11:59 +0800 Subject: [PATCH 052/108] =?UTF-8?q?=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiDataTypeDescriptor.cs | 2 +- .../Contexts/ApiRequestContext.cs | 2 +- .../Contexts/HttpContext.cs | 2 +- .../HttpApiOptions.cs | 8 +++--- .../HttpApiRequestMessage.cs | 14 +++++----- .../IDataCollection.cs | 4 +-- .../KeyValueSerializerOptions.cs | 4 +-- WebApiClientCore.Analyzers/Descriptors.cs | 2 +- WebApiClientCore.Analyzers/HttpApiContext.cs | 24 ++++++++--------- .../HttpApiDiagnosticAnalyzer.cs | 4 +-- .../CtorAttributeDiagnosticProvider.cs | 8 +++--- .../Providers/ModifierDiagnosticProvider.cs | 4 +-- ...=> NotMethodDefendedDiagnosticProvider.cs} | 6 ++--- .../SourceGenerator/HttpApiSyntaxReceiver.cs | 24 ++++++++--------- .../Attributes/JsonRpcMethodAttribute.cs | 2 +- .../Internals/JsonRpcContent.cs | 2 +- .../Internals/JsonRpcParameters.cs | 2 +- .../Internals/JsonRpcRequest.cs | 2 +- .../JsonRpcResult.cs | 2 +- .../Attributes/JsonNetContentAttribute.cs | 4 +-- .../ClientCredentialsTokenAttribute.cs | 4 +-- .../Attributes/OAuthTokenAttribute.cs | 12 ++++----- .../PasswordCredentialsTokenAttribute.cs | 4 +-- .../ITokenProviderBuilder.cs | 2 +- .../TokenHandlerExtensions.cs | 14 +++++----- ...TokenProviderExtensions.GrantTypeClient.cs | 6 ++--- ...kenProviderExtensions.GrantTypePassword.cs | 6 ++--- .../TokenProviderExtensions.cs | 12 ++++----- .../Exceptions/TokenEndPointNullException.cs | 4 +-- .../Exceptions/TokenNullException.cs | 4 +-- .../HttpMessageHandlers/OAuthTokenHandler.cs | 6 ++--- .../ITokenProvider.cs | 6 ++--- .../ITokenProviderFactory.cs | 4 +-- .../ITokenProviderService.cs | 4 +-- .../TokenProviderFactory.cs | 14 +++++----- .../TokenProviderFactoryOptions.cs | 6 ++--- .../TokenProviderService.cs | 8 +++--- .../ClientCredentialsOptions.cs | 6 ++--- .../ClientCredentialsTokenProvider.cs | 12 +++------ .../TokenProviders/DelegateTokenProvider.cs | 10 +++---- .../TokenProviders/OAuth2TokenClient.cs | 22 +++++----------- .../PasswordCredentialsOptions.cs | 6 ++--- .../PasswordCredentialsTokenProvider.cs | 14 ++++------ .../TokenProviders/RefreshTokenCredentials.cs | 4 +-- .../TokenProviders/TokenProvider.cs | 26 ++++++++----------- .../TokenResult.cs | 10 +++---- .../HttpRequestHeaderExtensionsTest.cs | 2 -- 47 files changed, 160 insertions(+), 190 deletions(-) rename WebApiClientCore.Analyzers/Providers/{NotMethodDefindedDiagnosticProvider.cs => NotMethodDefendedDiagnosticProvider.cs} (87%) 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/Contexts/ApiRequestContext.cs b/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs index c3191689..27679f15 100644 --- a/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs +++ b/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs @@ -9,7 +9,7 @@ namespace WebApiClientCore public class ApiRequestContext { /// - /// 获取http上下文 + /// 获取 http 上下文 /// public HttpContext HttpContext { get; } 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 22d4ef3b..161d84ec 100644 --- a/WebApiClientCore.Abstractions/HttpApiOptions.cs +++ b/WebApiClientCore.Abstractions/HttpApiOptions.cs @@ -17,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; } @@ -45,12 +45,12 @@ public class HttpApiOptions /// - /// 获取json序列化选项 + /// 获取 json 序列化选项 /// public JsonSerializerOptions JsonSerializeOptions { get; } = CreateJsonSerializeOptions(); /// - /// 获取json反序列化选项 + /// 获取 json 反序列化选项 /// public JsonSerializerOptions JsonDeserializeOptions { get; } = CreateJsonDeserializeOptions(); @@ -65,7 +65,7 @@ public class HttpApiOptions public XmlReaderSettings XmlDeserializeOptions { get; } = new XmlReaderSettings(); /// - /// 获取keyValue序列化选项 + /// 获取 keyValue 序列化选项 /// public KeyValueSerializerOptions KeyValueSerializeOptions { get; } = new KeyValueSerializerOptions(); diff --git a/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs b/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs index 3de36e79..51b50650 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,7 +29,7 @@ public abstract class HttpApiRequestMessage : HttpRequestMessage /// /// 添加字段到已有的Content - /// 要求content-type为application/x-www-form-urlencoded + /// 要求 content-type 为 application/x-www-form-urlencoded /// /// 名称 /// 值 @@ -42,7 +42,7 @@ public async Task AddFormFieldAsync(string name, string? value) /// /// 添加字段到已有的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/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/Serialization/KeyValueSerializerOptions.cs b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs index cc63c3e7..1862176a 100644 --- a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs +++ b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs @@ -14,7 +14,7 @@ namespace WebApiClientCore.Serialization public sealed class KeyValueSerializerOptions : KeyNamingOptions { /// - /// 包装的jsonOptions + /// 包装的 jsonOptions /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly JsonSerializerOptions jsonOptions; @@ -37,7 +37,7 @@ public JsonNamingPolicy? DictionaryKeyPolicy } /// - /// 获取或设置是否忽略null值 + /// 获取或设置是否忽略 null 值 /// public bool IgnoreNullValues { 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/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/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.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.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs index 26efd7a4..cb68e50c 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,7 +32,7 @@ public string CharSet } /// - /// 设置参数到http请求内容 + /// 设置参数到 http 请求内容 /// /// /// 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/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 813afac5..65788b57 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenHandlerExtensions.cs @@ -6,12 +6,12 @@ namespace Microsoft.Extensions.DependencyInjection { /// - /// 提供OAuth授权token应用的http消息处理程序扩展 + /// 提供OAuth授权 token 应用的 http 消息处理程序扩展 /// public static class TokenHandlerExtensions { /// - /// 添加token应用的http消息处理程序 + /// 添加 token 应用的 http 消息处理程序 /// 需要为接口或接口的基础接口注册TokenProvider /// /// @@ -27,7 +27,7 @@ public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder bu } /// - /// 添加token应用的http消息处理程序 + /// 添加 token 应用的 http 消息处理程序 /// 需要为接口或接口的基础接口注册TokenProvider /// /// @@ -36,19 +36,17 @@ public static IHttpClientBuilder AddOAuthTokenHandler(this IHttpClientBuilder bu /// /// /// - /// hanlder的创建委托 + /// handler的创建委托 /// token提供者的查找模式 /// -#if NET5_0_OR_GREATER - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "类型httpApiType明确是不会被裁剪的")] -#endif + [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 3e54015a..1489867c 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypeClient.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection public static partial class TokenProviderExtensions { /// - /// 为指定接口添加Client模式的token提供者 + /// 为指定接口添加Client模式的 token 提供者 /// /// 接口类型 /// @@ -26,7 +26,7 @@ public static partial class TokenProviderExtensions } /// - /// 为指定接口添加Client模式的token提供者 + /// 为指定接口添加Client模式的 token 提供者 /// /// 接口类型 /// @@ -40,7 +40,7 @@ public static partial class TokenProviderExtensions } /// - /// 为指定接口添加Client模式的token提供者 + /// 为指定接口添加Client模式的 token 提供者 /// /// 接口类型 /// diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs index bb5aa2df..2c658e36 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.GrantTypePassword.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection public static partial class TokenProviderExtensions { /// - /// 为指定接口添加Password模式的token提供者 + /// 为指定接口添加Password模式的 token 提供者 /// /// 接口类型 /// @@ -26,7 +26,7 @@ public static partial class TokenProviderExtensions } /// - /// 为指定接口添加Password模式的token提供者 + /// 为指定接口添加Password模式的 token 提供者 /// /// 接口类型 /// @@ -40,7 +40,7 @@ public static partial class TokenProviderExtensions } /// - /// 为指定接口添加Password模式的token提供者 + /// 为指定接口添加Password模式的 token 提供者 /// /// 接口类型 /// diff --git a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs index c22442eb..a486023d 100644 --- a/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs +++ b/WebApiClientCore.Extensions.OAuths/DependencyInjection/TokenProviderExtensions.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection public static partial class TokenProviderExtensions { /// - /// 为指定接口添加token提供者 + /// 为指定接口添加 token 提供者 /// /// 接口类型 /// @@ -30,7 +30,7 @@ public static partial class TokenProviderExtensions } /// - /// 为指定接口添加token提供者 + /// 为指定接口添加 token 提供者 /// /// 接口类型 /// token提供者类型 @@ -53,7 +53,7 @@ public static ITokenProviderBuilder AddTokenProvider< /// - /// 为指定接口添加token提供者 + /// 为指定接口添加 token 提供者 /// /// 接口类型 /// token提供者类型 @@ -74,14 +74,14 @@ public static ITokenProviderBuilder AddTokenProvider< /// - /// 向token工厂提供者添加token提供者 + /// 向 token 工厂提供者添加 token 提供者 /// /// 接口类型 /// token提供者类型 /// /// TokenProvider的别名 /// - private static ITokenProviderBuilder AddTokenProviderCore< + private static TokenProviderBuilder AddTokenProviderCore< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] THttpApi, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTokenProvider>( this IServiceCollection services, @@ -115,7 +115,7 @@ private static ITokenProviderBuilder AddTokenProviderCore< private class TokenProviderBuilder : ITokenProviderBuilder { /// - /// 获取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 965ab5dd..28b60e96 100644 --- a/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/ITokenProviderFactory.cs @@ -9,7 +9,7 @@ namespace WebApiClientCore.Extensions.OAuths public interface ITokenProviderFactory { /// - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 @@ -20,7 +20,7 @@ ITokenProvider Create( TypeMatchMode typeMatchMode = TypeMatchMode.TypeOnly); /// - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 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 ed04f04e..402b8005 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviderFactory.cs @@ -7,7 +7,7 @@ namespace WebApiClientCore.Extensions.OAuths { /// - /// 表示默认的token提供者工厂 + /// 表示默认的 token 提供者工厂 /// sealed class TokenProviderFactory : ITokenProviderFactory { @@ -16,7 +16,7 @@ sealed class TokenProviderFactory : ITokenProviderFactory private readonly ConcurrentDictionary tokenProviderCache = new(); /// - /// 默认的token提供者工厂 + /// 默认的 token 提供者工厂 /// /// /// @@ -27,7 +27,7 @@ public TokenProviderFactory(IServiceProvider serviceProvider, IOptions - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 @@ -42,7 +42,7 @@ public ITokenProvider Create( } /// - /// 通过接口类型获取或创建其对应的token提供者 + /// 通过接口类型获取或创建其对应的 token 提供者 /// /// 接口类型 /// 类型匹配模式 @@ -68,7 +68,7 @@ public ITokenProvider Create( } /// - /// 创建其对应的token提供者 + /// 创建其对应的 token 提供者 /// /// 缓存的键 /// @@ -95,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); } 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 21e1da75..83f122a5 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/ClientCredentialsTokenProvider.cs @@ -7,12 +7,12 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示Client模式的token提供者 + /// 表示Client模式的 token 提供者 /// public class ClientCredentialsTokenProvider : TokenProvider { /// - /// Client模式的token提供者 + /// Client模式的 token 提供者 /// /// public ClientCredentialsTokenProvider(IServiceProvider services) @@ -21,14 +21,12 @@ public ClientCredentialsTokenProvider(IServiceProvider services) } /// - /// 请求获取token + /// 请求获取 token /// /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif protected override Task RequestTokenAsync(IServiceProvider serviceProvider) { var options = this.GetOptionsValue(); @@ -42,15 +40,13 @@ public ClientCredentialsTokenProvider(IServiceProvider services) } /// - /// 刷新token + /// 刷新 token /// /// /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif 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 14d2fa61..83c856cd 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs @@ -31,51 +31,45 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor - /// 以client_credentials授权方式获取token + /// 以 client_credentials 授权方式获取 token /// /// token请求地址 /// 身份信息 /// [HttpPost] [FormField("grant_type", "client_credentials")] -#if NET8_0_OR_GREATER [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.")] -#endif 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")] -#if NET8_0_OR_GREATER [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.")] -#endif 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")] -#if NET8_0_OR_GREATER [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.")] -#endif public Task RefreshTokenAsync([Required, Uri] Uri endpoint, [Required, FormContent] RefreshTokenCredentials credentials) { return this.PostFormAsync(endpoint, "refresh_token", credentials); @@ -89,10 +83,8 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor /// /// -#if NET8_0_OR_GREATER [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.")] -#endif private async Task PostFormAsync(Uri endpoint, string grant_type, TCredentials credentials) { using var formContent = new FormContent(credentials, this.httpApiOptions.KeyValueSerializeOptions); @@ -100,11 +92,9 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor(utf8Json, this.httpApiOptions.JsonDeserializeOptions); + return utf8Json.Length == 0 + ? default + : JsonSerializer.Deserialize(utf8Json, 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 1932b665..ff188a93 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/PasswordCredentialsTokenProvider.cs @@ -7,12 +7,12 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders { /// - /// 表示Password模式的token提供者 + /// 表示 Password 模式的 token 提供者 /// public class PasswordCredentialsTokenProvider : TokenProvider { /// - /// Password模式的token提供者 + /// Password 模式的 token 提供者 /// /// public PasswordCredentialsTokenProvider(IServiceProvider services) @@ -21,14 +21,12 @@ public PasswordCredentialsTokenProvider(IServiceProvider services) } /// - /// 请求获取token + /// 请求获取 token /// /// /// -#if NET5_0_OR_GREATER [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 = "")] -#endif protected override Task RequestTokenAsync(IServiceProvider serviceProvider) { var options = this.GetOptionsValue(); @@ -42,15 +40,13 @@ public PasswordCredentialsTokenProvider(IServiceProvider services) } /// - /// 刷新token + /// 刷新 token /// /// /// - /// -#if NET5_0_OR_GREATER + /// [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 = "")] -#endif 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.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs b/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs index 9fab7297..8b8d0984 100644 --- a/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs +++ b/WebApiClientCore.Test/BuildinExtensions/HttpRequestHeaderExtensionsTest.cs @@ -13,7 +13,6 @@ public void ToHeaderNameTest() Assert.Equal("Accept", HttpRequestHeader.Accept.ToHeaderName()); Assert.Equal("Accept-Charset", HttpRequestHeader.AcceptCharset.ToHeaderName()); -#if NET5_0_OR_GREATER foreach (var item in Enum.GetValues()) { var name = Enum.GetName(item); @@ -21,7 +20,6 @@ public void ToHeaderNameTest() var headerName = field?.GetCustomAttribute()?.Name; Assert.Equal(headerName, item.ToHeaderName()); } -#endif } } } \ No newline at end of file From 6b28aecb662223ce4d42ce52fcda80e531125b9a Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sat, 15 Jun 2024 21:30:46 +0800 Subject: [PATCH 053/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WebApiClientCore.Abstractions.csproj | 2 +- .../WebApiClientCore.Analyzers.csproj | 2 +- .../WebApiClientCore.Extensions.JsonRpc.csproj | 2 +- .../WebApiClientCore.Extensions.NewtonsoftJson.csproj | 2 +- .../WebApiClientCore.Extensions.OAuths.csproj | 6 +++--- WebApiClientCore/WebApiClientCore.csproj | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj index f36dc8e1..b0e1a9b8 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -7,7 +7,7 @@ WebApiClientCore WebApiClientCore.Abstractions - $(OutputPath)\$(AssemblyName).xml + True WebApiClientCore的接口与抽象类型 WebApiClientCore的接口与抽象类型 diff --git a/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj b/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj index 4bd7c2bf..55ddda26 100644 --- a/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj +++ b/WebApiClientCore.Analyzers/WebApiClientCore.Analyzers.csproj @@ -3,7 +3,7 @@ enable netstandard2.0 - $(OutputPath)\$(AssemblyName).xml + True false false true diff --git a/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj b/WebApiClientCore.Extensions.JsonRpc/WebApiClientCore.Extensions.JsonRpc.csproj index 672fb9b2..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 - $(OutputPath)\$(AssemblyName).xml + True true Sign.snk diff --git a/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj b/WebApiClientCore.Extensions.NewtonsoftJson/WebApiClientCore.Extensions.NewtonsoftJson.csproj index 59824ea2..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 - $(OutputPath)\$(AssemblyName).xml + True true Sign.snk diff --git a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj index 0e9ad98e..d9905876 100644 --- a/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj +++ b/WebApiClientCore.Extensions.OAuths/WebApiClientCore.Extensions.OAuths.csproj @@ -1,9 +1,9 @@ - enable - netstandard2.1;net5.0;net8.0 - $(OutputPath)\$(AssemblyName).xml + enable + True + netstandard2.1;net5.0;net8.0 true true diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 1f4102e0..67b4911d 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -1,11 +1,11 @@  - enable + enable + True netstandard2.1;net5.0;net8.0 - $(OutputPath)\$(AssemblyName).xml true - + .NetCore声明式的Http客户端库 一款基于HttpClient封装,只需要定义c#接口并修饰相关特性,即可异步调用远程http接口的客户端库 From 7c5d7c5ccbc00cae0e9a65c3de738d14421a6e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E4=B9=9D?= <366193849@qq.com> Date: Sun, 16 Jun 2024 22:44:34 +0800 Subject: [PATCH 054/108] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 46fa5988..f03a11d4 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ Supports parsing local or remote OpenApi documents to generate WebApiClientCore 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. ### Documentation support -https://webapiclient.github.io/ +[https://webapiclient.github.io/](https://webapiclient.github.io/en/) From b78a736d26459bb9e5c56b524e9cb3d8c0a128c4 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sun, 16 Jun 2024 23:59:34 +0800 Subject: [PATCH 055/108] =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/ApiRequestSender.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/WebApiClientCore/Implementations/ApiRequestSender.cs b/WebApiClientCore/Implementations/ApiRequestSender.cs index 4c784073..b6258665 100644 --- a/WebApiClientCore/Implementations/ApiRequestSender.cs +++ b/WebApiClientCore/Implementations/ApiRequestSender.cs @@ -199,7 +199,7 @@ public ActionCache(string key, HttpResponseMessage? value) /// public CancellationTokenLinker(IList tokenList) { - if (IsNoneCancellationToken(tokenList) == true) + if (IsNoneCancellationToken(tokenList)) { this.tokenSource = null; this.Token = CancellationToken.None; @@ -218,15 +218,8 @@ public CancellationTokenLinker(IList tokenList) /// private static bool IsNoneCancellationToken(IList tokenList) { - if (tokenList.Count == 0) - { - return true; - } - if (tokenList.Count == 1 && tokenList[0] == CancellationToken.None) - { - return true; - } - return false; + var count = tokenList.Count; + return (count == 0) || (count == 1 && tokenList[0] == CancellationToken.None); } /// @@ -234,10 +227,7 @@ private static bool IsNoneCancellationToken(IList tokenList) /// public void Dispose() { - if (this.tokenSource != null) - { - this.tokenSource.Dispose(); - } + this.tokenSource?.Dispose(); } } } From 844e9bd146be9307ea2b52151c995febc52c6b6e Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Mon, 17 Jun 2024 23:12:58 +0800 Subject: [PATCH 056/108] =?UTF-8?q?ApiResponseContext=E5=A2=9E=E5=8A=A0Req?= =?UTF-8?q?uestAborted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contexts/ApiResponseContext.cs | 17 ++++ .../TokenProviders/OAuth2TokenClient.cs | 6 +- .../ApiResponseContextExtensions.cs | 35 ++----- .../Implementations/ApiRequestExecuter.cs | 60 +++++++++++- .../Implementations/ApiRequestSender.cs | 71 ++------------- .../System.Net.Http/HttpContentExtensions.cs | 91 ++++++++++++++++++- 6 files changed, 179 insertions(+), 101 deletions(-) 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.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs index 83c856cd..f5b4d3d8 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Net.Http; -using System.Text.Json; using System.Threading.Tasks; using WebApiClientCore.Attributes; using WebApiClientCore.HttpContents; @@ -91,10 +90,7 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor(utf8Json, this.httpApiOptions.JsonDeserializeOptions); + return await response.Content.ReadAsJsonAsync(this.httpApiOptions.JsonDeserializeOptions); } } } diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 8b6e761f..6216a317 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Net.Http; -using System.Text; -using System.Text.Json; using System.Threading.Tasks; using WebApiClientCore.Serialization; @@ -22,7 +20,7 @@ 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; if (response == null) { @@ -30,32 +28,10 @@ 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.ReadAsJsonAsync(objType, options, context.RequestAborted); } - + /// /// 使用Xml反序列化响应内容为目标类型 /// @@ -73,7 +49,12 @@ public static class ApiResponseContextExtensions var content = response.Content; var options = context.HttpContext.HttpApiOptions.XmlDeserializeOptions; + +#if NET5_0_OR_GREATER + var xml = await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false); +#else var xml = await content.ReadAsStringAsync().ConfigureAwait(false); +#endif return XmlSerializer.Deserialize(xml, objType, options); } } diff --git a/WebApiClientCore/Implementations/ApiRequestExecuter.cs b/WebApiClientCore/Implementations/ApiRequestExecuter.cs index 2385fb81..0ba82447 100644 --- a/WebApiClientCore/Implementations/ApiRequestExecuter.cs +++ b/WebApiClientCore/Implementations/ApiRequestExecuter.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace WebApiClientCore.Implementations @@ -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 b6258665..de925545 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; @@ -17,9 +15,10 @@ static class ApiRequestSender /// 发送 http 请求 /// /// + /// /// /// - public static async Task SendAsync(ApiRequestContext context) + public static async Task SendAsync(ApiRequestContext context, CancellationToken requestAborted) { if (context.HttpContext.RequestMessage.RequestUri == null) { @@ -28,12 +27,12 @@ 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 }; } } @@ -41,9 +40,10 @@ public static async Task SendAsync(ApiRequestContext context /// 发送 http 请求 /// /// + /// /// /// - private static async Task SendCoreAsync(ApiRequestContext context) + private static async Task SendCoreAsync(ApiRequestContext context, CancellationToken requestAborted) { var actionCache = await context.GetCacheAsync().ConfigureAwait(false); if (actionCache != null && actionCache.Value != null) @@ -56,9 +56,7 @@ private static async Task SendCoreAsync(ApiRequestContext context) 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 response = await client.SendAsync(request, completionOption, requestAborted).ConfigureAwait(false); context.HttpContext.ResponseMessage = response; await context.SetCacheAsync(actionCache?.Key, response).ConfigureAwait(false); } @@ -177,58 +175,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)) - { - 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/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index 3c915fdc..ef861ff9 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,7 +1,11 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; +using WebApiClientCore; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -68,6 +72,86 @@ public static void EnsureNotBuffered(this HttpContent httpContent) { throw new HttpContentBufferedException(); } + } + + /// + /// 读取 json 内容为指定的类型 + /// + /// http内容 + /// 目标类型 + /// 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 async Task ReadAsJsonAsync(this HttpContent content, Type objType, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + { +#if NET5_0_OR_GREATER + return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, objType, options, cancellationToken); +#else + var encoding = content.GetEncoding(); + 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); + } +#endif + } + + /// + /// 读取 json 内容为指定的类型 + /// + /// 目标类型 + /// http内容 + /// 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 async Task ReadAsJsonAsync(this HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + { +#if NET5_0_OR_GREATER + return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, options, cancellationToken); +#else + var encoding = content.GetEncoding(); + if (Encoding.UTF8.Equals(encoding) == false) + { + var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); + if (byteArray.Length == 0) + { + return default; + } + var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); + return JsonSerializer.Deserialize(utf8Json, options); + } + + if (content.IsBuffered() == false) + { + var utf8Json = await content.ReadAsStreamAsync().ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(utf8Json, options).ConfigureAwait(false); + } + else + { + var utf8Json = await content.ReadAsByteArrayAsync().ConfigureAwait(false); + return utf8Json.Length == 0 ? default : JsonSerializer.Deserialize(utf8Json, options); + } +#endif } /// @@ -92,10 +176,7 @@ public static async Task ReadAsByteArrayAsync(this HttpContent httpConte { var encoding = httpContent.GetEncoding(); var byteArray = await httpContent.ReadAsByteArrayAsync().ConfigureAwait(false); - - return encoding.Equals(dstEncoding) - ? byteArray - : Encoding.Convert(encoding, dstEncoding, byteArray); + return encoding.Equals(dstEncoding) ? byteArray : Encoding.Convert(encoding, dstEncoding, byteArray); } /// From 401c41a4d5381349d32463074367dc7f34b44405 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Mon, 17 Jun 2024 23:59:32 +0800 Subject: [PATCH 057/108] =?UTF-8?q?=E7=AE=80=E5=8C=96ReadAsJsonAsync?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../System.Net.Http/HttpContentExtensions.cs | 92 +++++++++---------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index ef861ff9..5c8ec631 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -72,86 +72,51 @@ public static void EnsureNotBuffered(this HttpContent httpContent) { throw new HttpContentBufferedException(); } - } + } + /// /// 读取 json 内容为指定的类型 /// + /// 目标类型 /// http内容 - /// 目标类型 /// 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 async Task ReadAsJsonAsync(this HttpContent content, Type objType, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + public static async Task ReadAsJsonAsync(this HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { #if NET5_0_OR_GREATER - return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, objType, options, cancellationToken); -#else - var encoding = content.GetEncoding(); - 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 System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, options, cancellationToken).ConfigureAwait(false); } #endif + var utf8Json = await content.ReadAsUtf8ByteArrayAsync(cancellationToken).ConfigureAwait(false); + return utf8Json.Length == 0 ? default : JsonSerializer.Deserialize(utf8Json, options); } /// /// 读取 json 内容为指定的类型 /// - /// 目标类型 /// http内容 + /// 目标类型 /// 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 async Task ReadAsJsonAsync(this HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + public static async Task ReadAsJsonAsync(this HttpContent content, Type objType, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { #if NET5_0_OR_GREATER - return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, options, cancellationToken); -#else - var encoding = content.GetEncoding(); - if (Encoding.UTF8.Equals(encoding) == false) - { - var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - if (byteArray.Length == 0) - { - return default; - } - var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); - return JsonSerializer.Deserialize(utf8Json, options); - } - if (content.IsBuffered() == false) { - var utf8Json = await content.ReadAsStreamAsync().ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(utf8Json, options).ConfigureAwait(false); - } - else - { - var utf8Json = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - return utf8Json.Length == 0 ? default : JsonSerializer.Deserialize(utf8Json, options); + return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, objType, options, cancellationToken).ConfigureAwait(false); } #endif + var utf8Json = await content.ReadAsUtf8ByteArrayAsync(cancellationToken).ConfigureAwait(false); + return utf8Json.Length == 0 ? objType.DefaultValue() : JsonSerializer.Deserialize(utf8Json, objType, options); } /// @@ -162,7 +127,31 @@ public static void EnsureNotBuffered(this HttpContent httpContent) /// public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent) { - return httpContent.ReadAsByteArrayAsync(Encoding.UTF8); + return httpContent.ReadAsByteArrayAsync(Encoding.UTF8, default); + } + + /// + /// 读取为二进制数组并转换为 utf8 编码 + /// + /// + /// + /// + /// + public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent, CancellationToken cancellationToken) + { + return httpContent.ReadAsByteArrayAsync(Encoding.UTF8, cancellationToken); + } + + /// + /// 读取为二进制数组并转换为指定的编码 + /// + /// + /// 目标编码 + /// + /// + public static Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding) + { + return httpContent.ReadAsByteArrayAsync(dstEncoding, default); } /// @@ -170,12 +159,17 @@ public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent /// /// /// 目标编码 + /// /// /// - public static async Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding) + public static async Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding, CancellationToken cancellationToken) { var encoding = httpContent.GetEncoding(); +#if NET5_0_OR_GREATER + var byteArray = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); +#else var byteArray = await httpContent.ReadAsByteArrayAsync().ConfigureAwait(false); +#endif return encoding.Equals(dstEncoding) ? byteArray : Encoding.Convert(encoding, dstEncoding, byteArray); } From 0b25095598f79dc49975ea735d6336dcc0391a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Tue, 18 Jun 2024 09:09:05 +0800 Subject: [PATCH 058/108] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilterAttributes/LoggingFilterAttribute.cs | 2 +- WebApiClientCore/Internals/ValueStringBuilder.cs | 11 +++++++++++ .../System.Net.Http/HttpContentExtensions.cs | 13 ++++++++----- .../System.Net.Http/HttpRequestMessageExtensions.cs | 5 +++-- .../HttpResponseMessageExtensions.cs | 10 ++++++---- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index 19b23498..926ca42b 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -97,7 +97,7 @@ 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) { diff --git a/WebApiClientCore/Internals/ValueStringBuilder.cs b/WebApiClientCore/Internals/ValueStringBuilder.cs index 6eaa64a7..db4eeed7 100644 --- a/WebApiClientCore/Internals/ValueStringBuilder.cs +++ b/WebApiClientCore/Internals/ValueStringBuilder.cs @@ -61,6 +61,17 @@ public void Append(ReadOnlySpan value) this.index = newSize; } + /// + /// 添加 chars 并换行 + /// + /// + /// + public void AppendLine(ReadOnlySpan value) + { + this.Append(value); + this.Append(Environment.NewLine); + } + /// /// 扩容 /// diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index 5c8ec631..ae7ad4ec 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -180,19 +180,22 @@ 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/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..2e37c1f2 100644 --- a/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs @@ -1,7 +1,7 @@ using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; +using WebApiClientCore.Internals; namespace System.Net.Http { @@ -17,9 +17,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) { From 1224e52dabfca15e0d5cbe1a7ac465301e35971a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Tue, 18 Jun 2024 14:20:38 +0800 Subject: [PATCH 059/108] add SerializeToJsonString --- .../ApiParameterContextExtensions.cs | 20 +++++++++++++++++++ .../JsonFormDataTextAttribute.cs | 5 +---- .../JsonFormFieldAttribute.cs | 6 ++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/WebApiClientCore/ApiParameterContextExtensions.cs b/WebApiClientCore/ApiParameterContextExtensions.cs index 07827f59..2bd46e77 100644 --- a/WebApiClientCore/ApiParameterContextExtensions.cs +++ b/WebApiClientCore/ApiParameterContextExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Text.Json; using WebApiClientCore.Internals; using WebApiClientCore.Serialization; @@ -61,6 +62,25 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit 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 /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs index 85048061..654125ca 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormDataTextAttribute.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Threading.Tasks; namespace WebApiClientCore.Attributes @@ -18,11 +17,9 @@ public class JsonFormDataTextAttribute : ApiParameterAttribute [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 5792a01d..3647b0ed 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonFormFieldAttribute.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Threading.Tasks; namespace WebApiClientCore.Attributes @@ -17,10 +16,9 @@ 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); } } From 18226c8eaa2c434849de6748d0ab488298da5599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Tue, 18 Jun 2024 15:12:49 +0800 Subject: [PATCH 060/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Requests/MockResponseHandler.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs b/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs index c12b3497..140903b7 100644 --- a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs +++ b/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs @@ -11,17 +11,25 @@ namespace WebApiClientCore.Benchmarks.Requests /// class MockResponseHandler : DelegatingHandler { - private readonly byte[] json; + private static readonly byte[] jsonByteArray; - public MockResponseHandler() + static MockResponseHandler() { - var model = new Model { A = "A", B = 2, C = 3d }; - this.json = JsonSerializer.SerializeToUtf8Bytes(model); + var model = new Model + { + A = "A", + B = 2, + C = 3d + }; + jsonByteArray = JsonSerializer.SerializeToUtf8Bytes(model); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayJsonContent(this.json) }; + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayJsonContent(jsonByteArray) + }; return Task.FromResult(response); } } From a2272c43a603e581132bfda77006e1c869953e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Tue, 18 Jun 2024 18:21:46 +0800 Subject: [PATCH 061/108] =?UTF-8?q?=E7=AE=80=E5=8C=96ReadAsJson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Requests/GetBenchmark.cs | 9 ++---- .../Requests/MockResponseHandler.cs | 6 ++-- .../Requests/PostJsonBenchmark.cs | 13 ++------ .../System.Net.Http/HttpContentExtensions.cs | 32 ++++++++++++++----- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs index 2e9be924..08649366 100644 --- a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; using System.Net.Http; -using System.Text.Json; +using System.Net.Http.Json; using System.Threading.Tasks; namespace WebApiClientCore.Benchmarks.Requests @@ -23,13 +23,10 @@ public async Task HttpClient_GetAsync() 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); + var requestUri = $"http://webapiclient.com/{id}"; + return await httpClient.GetFromJsonAsync(requestUri); } - /// /// 使用WebApiClientCore请求 /// diff --git a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs b/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs index 140903b7..c939ba41 100644 --- a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs +++ b/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.IO; +using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -26,9 +27,10 @@ static MockResponseHandler() protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + var content = new StreamContent(new MemoryStream(jsonByteArray, writable: false)); var response = new HttpResponseMessage(HttpStatusCode.OK) { - Content = new ByteArrayJsonContent(jsonByteArray) + Content = content }; return Task.FromResult(response); } diff --git a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs index 167e83c2..40c8062e 100644 --- a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; using System.Net.Http; -using System.Text.Json; +using System.Net.Http.Json; using System.Threading.Tasks; namespace WebApiClientCore.Benchmarks.Requests @@ -23,15 +23,8 @@ public async Task HttpClient_PostJsonAsync() 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); + var response = await httpClient.PostAsJsonAsync($"http://webapiclient.com/", input); + return await response.Content.ReadFromJsonAsync(); } /// diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index ae7ad4ec..0db7e8c1 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -88,13 +88,21 @@ public static void EnsureNotBuffered(this HttpContent httpContent) public static async Task ReadAsJsonAsync(this HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { #if NET5_0_OR_GREATER - if (content.IsBuffered() == false) + return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, options, cancellationToken).ConfigureAwait(false); +#else + var encoding = content.GetEncoding(); + if (encoding.Equals(Encoding.UTF8)) + { + using var utf8Json = await content.ReadAsStreamAsync().ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(utf8Json, options, cancellationToken); + } + else { - return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, options, cancellationToken).ConfigureAwait(false); + var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); + var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); + return utf8Json.Length == 0 ? default : JsonSerializer.Deserialize(utf8Json, options); } #endif - var utf8Json = await content.ReadAsUtf8ByteArrayAsync(cancellationToken).ConfigureAwait(false); - return utf8Json.Length == 0 ? default : JsonSerializer.Deserialize(utf8Json, options); } /// @@ -110,13 +118,21 @@ public static void EnsureNotBuffered(this HttpContent httpContent) public static async Task ReadAsJsonAsync(this HttpContent content, Type objType, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { #if NET5_0_OR_GREATER - if (content.IsBuffered() == false) + return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, objType, options, cancellationToken).ConfigureAwait(false); +#else + var encoding = content.GetEncoding(); + if (encoding.Equals(Encoding.UTF8)) + { + using var stream = await content.ReadAsStreamAsync().ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(stream, objType, options, cancellationToken); + } + else { - return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, objType, options, cancellationToken).ConfigureAwait(false); + var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); + var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); + return utf8Json.Length == 0 ? objType.DefaultValue() : JsonSerializer.Deserialize(utf8Json, objType, options); } #endif - var utf8Json = await content.ReadAsUtf8ByteArrayAsync(cancellationToken).ConfigureAwait(false); - return utf8Json.Length == 0 ? objType.DefaultValue() : JsonSerializer.Deserialize(utf8Json, objType, options); } /// From aaefc67b00303a87c7a4d99a624206c0c1761efb Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Wed, 19 Jun 2024 01:07:58 +0800 Subject: [PATCH 062/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0benchmark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Buffers/Benchmark.cs | 21 ------ .../CreateInstances/Benchmarks.cs | 34 --------- WebApiClientCore.Benchmarks/IBenchmark.cs | 10 --- .../Others/ReadAsJsonBenchamrk.cs | 36 +++++++++ WebApiClientCore.Benchmarks/Program.cs | 15 ++-- .../Requests/Benchmark.cs | 31 ++++++-- .../Requests/ByteArrayJsonContent.cs | 21 ------ .../{GetBenchmark.cs => HttpGetBenchmark.cs} | 29 ++------ .../Requests/HttpPostJsonBenchmark.cs | 39 ++++++++++ .../Requests/HttpPutFormBenchmark.cs | 25 +++++++ .../Requests/IRefitApi.cs | 6 +- .../Requests/IWebApiClientCoreApi.cs | 7 +- .../Requests/MockResponseHandler.cs | 38 ---------- WebApiClientCore.Benchmarks/Requests/Model.cs | 11 --- .../Requests/PostJsonBenchmark.cs | 53 ------------- .../Requests/PutFormBenchmark.cs | 36 --------- .../StringReplaces/Benchmark.cs | 32 -------- WebApiClientCore.Benchmarks/User.cs | 37 ++++++++++ .../WebApiClientCore.Benchmarks.csproj | 10 ++- ...rks.Requests.GetBenchmark-report-github.md | 14 ---- ...equests.PostJsonBenchmark-report-github.md | 14 ---- ...Requests.PutFormBenchmark-report-github.md | 13 ---- WebApiClientCore.Benchmarks/user.json | 8 ++ WebApiClientCore/Internals/MediaTypeUtil.cs | 37 +--------- .../System.Net.Http/HttpContentExtensions.cs | 74 +++++++++---------- 25 files changed, 235 insertions(+), 416 deletions(-) delete mode 100644 WebApiClientCore.Benchmarks/Buffers/Benchmark.cs delete mode 100644 WebApiClientCore.Benchmarks/CreateInstances/Benchmarks.cs delete mode 100644 WebApiClientCore.Benchmarks/IBenchmark.cs create mode 100644 WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs delete mode 100644 WebApiClientCore.Benchmarks/Requests/ByteArrayJsonContent.cs rename WebApiClientCore.Benchmarks/Requests/{GetBenchmark.cs => HttpGetBenchmark.cs} (56%) create mode 100644 WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs create mode 100644 WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs delete mode 100644 WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs delete mode 100644 WebApiClientCore.Benchmarks/Requests/Model.cs delete mode 100644 WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs delete mode 100644 WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs delete mode 100644 WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs create mode 100644 WebApiClientCore.Benchmarks/User.cs delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md create mode 100644 WebApiClientCore.Benchmarks/user.json 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/Others/ReadAsJsonBenchamrk.cs b/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs new file mode 100644 index 00000000..e6ca538b --- /dev/null +++ b/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs @@ -0,0 +1,36 @@ +using BenchmarkDotNet.Attributes; +using System.IO; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Others +{ + [InProcess] + [MemoryDiagnoser] + public class ReadAsJsonBenchmark + { + [Benchmark(Baseline = true)] + public async Task ReadAsJsonAsync() + { + var content = new StreamContent(new MemoryStream(User.Utf8Array, writable: false)); + return await content.ReadAsJsonAsync(null, default); + } + + [Benchmark] + public async Task ReadFromJsonAsync() + { + var content = new StreamContent(new MemoryStream(User.Utf8Array, writable: false)); + return await content.ReadFromJsonAsync(default(JsonSerializerOptions)); + } + + [Benchmark] + public async Task ReadAsByteArrayAsync() + { + var content = new StreamContent(new MemoryStream(User.Utf8Array, writable: false)); + var utf8Json = await content.ReadAsUtf8ByteArrayAsync(); + return JsonSerializer.Deserialize(utf8Json, default(JsonSerializerOptions)); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index dddd8cf6..6dd0c705 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -1,21 +1,16 @@ using BenchmarkDotNet.Running; using System; -using System.Linq; +using WebApiClientCore.Benchmarks.Requests; namespace WebApiClientCore.Benchmarks { class Program { static void Main(string[] args) - { - var benchmarkTypes = typeof(Program).Assembly.GetTypes() - .Where(item => typeof(Requests.Benchmark).IsAssignableFrom(item)) - .Where(item => item.IsAbstract == false && item.IsClass); - - foreach (var item in benchmarkTypes) - { - BenchmarkRunner.Run(item); - } + { + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); Console.ReadLine(); } } diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 775769d1..15af8824 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -2,25 +2,29 @@ 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.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 void Setup() + public void GlobalSetup() { var services = new ServiceCollection(); services .AddHttpClient(typeof(HttpClient).FullName) - .AddHttpMessageHandler(() => new MockResponseHandler()); + .AddHttpMessageHandler(() => new UserResponseHandler()); services .AddHttpApi(o => @@ -28,19 +32,34 @@ public void Setup() o.UseParameterPropertyValidate = false; o.UseReturnValuePropertyValidate = false; }) - .AddHttpMessageHandler(() => new MockResponseHandler()) + .AddHttpMessageHandler(() => new UserResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services .AddRefitClient(new RefitSettings { }) - .AddHttpMessageHandler(() => new MockResponseHandler()) + .AddHttpMessageHandler(() => new UserResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); this.ServiceProvider = services.BuildServiceProvider(); this.ServiceProvider.GetService(); this.ServiceProvider.GetService(); } + + private class UserResponseHandler : 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.Utf8Array, writable: false), User.Utf8Array.Length); + content.Headers.ContentType = applicationJson; + 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/HttpGetBenchmark.cs similarity index 56% rename from WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs rename to WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs index 08649366..bf7f6031 100644 --- a/WebApiClientCore.Benchmarks/Requests/GetBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs @@ -6,46 +6,29 @@ namespace WebApiClientCore.Benchmarks.Requests { - /// - /// 跳过真实的http请求环节的模拟Get请求 - /// - [MemoryDiagnoser] - public class GetBenchmark : Benchmark + public class HttpGetBenchmark : Benchmark { - /// - /// 使用原生HttpClient请求 - /// - /// [Benchmark] - public async Task HttpClient_GetAsync() + public async Task HttpClient_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var httpClient = scope.ServiceProvider.GetRequiredService().CreateClient(typeof(HttpClient).FullName); var id = "id"; var requestUri = $"http://webapiclient.com/{id}"; - return await httpClient.GetFromJsonAsync(requestUri); + return await httpClient.GetFromJsonAsync(requestUri); } - /// - /// 使用WebApiClientCore请求 - /// - /// [Benchmark(Baseline = true)] - public async Task WebApiClientCore_GetAsync() + public async Task WebApiClientCore_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await benchmarkApi.GetAsync(id: "id"); + return await benchmarkApi.GetJsonAsync(id: "id"); } - - /// - /// Refit的Get请求 - /// - /// [Benchmark] - public async Task Refit_GetAsync() + public async Task Refit_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var benchmarkApi = scope.ServiceProvider.GetRequiredService(); diff --git a/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs new file mode 100644 index 00000000..60aef3ad --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs @@ -0,0 +1,39 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace WebApiClientCore.Benchmarks.Requests +{ + [MemoryDiagnoser] + public class HttpPostJsonBenchmark : Benchmark + { + [Benchmark] + public async Task HttpClient_PostJsonAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var httpClient = scope.ServiceProvider.GetRequiredService().CreateClient(typeof(HttpClient).FullName); + + var response = await httpClient.PostAsJsonAsync($"http://webapiclient.com/", User.Instance); + return await response.Content.ReadFromJsonAsync(); + } + + [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/HttpPutFormBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs new file mode 100644 index 00000000..abf7435e --- /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("id001", User.Instance); + } + + [Benchmark] + public async Task Refit_PutFormAsync() + { + using var scope = this.ServiceProvider.CreateScope(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + return await benchmarkApi.PutFormAsync("id001", User.Instance); + } + } +} diff --git a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs index fd5291db..c37e9613 100644 --- a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs @@ -6,12 +6,12 @@ namespace WebApiClientCore.Benchmarks.Requests public interface IRefitApi { [Get("/benchmarks/{id}")] - Task GetAsync(string id); + Task GetAsync(string id); [Post("/benchmarks")] - Task PostJsonAsync([Body(BodySerializationMethod.Serialized)]Model model); + Task PostJsonAsync([Body(BodySerializationMethod.Serialized)] User model); [Put("/benchmarks/{id}")] - Task PutFormAsync(string id, [Body(BodySerializationMethod.UrlEncoded)]Model model); + Task PutFormAsync(string id, [Body(BodySerializationMethod.UrlEncoded)] User model); } } diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs index 9346b240..7188d5e0 100644 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs @@ -3,15 +3,16 @@ namespace WebApiClientCore.Benchmarks.Requests { + [JsonReturn] public interface IWebApiClientCoreApi { [HttpGet("/benchmarks/{id}")] - Task GetAsync(string id); + Task GetJsonAsync(string id); [HttpPost("/benchmarks")] - Task PostJsonAsync([JsonContent] Model model); + Task PostJsonAsync([JsonContent] User model); [HttpPut("/benchmarks/{id}")] - Task PutFormAsync(string id, [FormContent] Model model); + Task PutFormAsync(string id, [FormContent] User model); } } diff --git a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs b/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs deleted file mode 100644 index c939ba41..00000000 --- a/WebApiClientCore.Benchmarks/Requests/MockResponseHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.IO; -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 static readonly byte[] jsonByteArray; - - static MockResponseHandler() - { - var model = new Model - { - A = "A", - B = 2, - C = 3d - }; - jsonByteArray = JsonSerializer.SerializeToUtf8Bytes(model); - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var content = new StreamContent(new MemoryStream(jsonByteArray, writable: false)); - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = content - }; - 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 40c8062e..00000000 --- a/WebApiClientCore.Benchmarks/Requests/PostJsonBenchmark.cs +++ /dev/null @@ -1,53 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; -using System.Net.Http.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 response = await httpClient.PostAsJsonAsync($"http://webapiclient.com/", input); - return await response.Content.ReadFromJsonAsync(); - } - - /// - /// 使用WebApiClientCore请求 - /// - /// - [Benchmark(Baseline = true)] - public async Task WebApiClientCore_PostJsonAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await benchmarkApi.PostJsonAsync(input); - } - - - [Benchmark] - public async Task Refit_PostJsonAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await benchmarkApi.PostJsonAsync(input); - } - } -} diff --git a/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/PutFormBenchmark.cs deleted file mode 100644 index ec8c7c63..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 benchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await benchmarkApi.PutFormAsync("id001", input); - } - - - [Benchmark] - public async Task Refit_PutFormAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - var input = new Model { A = "a" }; - return await benchmarkApi.PutFormAsync("id001", input); - } - } -} diff --git a/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs b/WebApiClientCore.Benchmarks/StringReplaces/Benchmark.cs deleted file mode 100644 index 5fb83c99..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.ReplaceIgnoreCase(pattern, replacement, out var _); - } - } -} diff --git a/WebApiClientCore.Benchmarks/User.cs b/WebApiClientCore.Benchmarks/User.cs new file mode 100644 index 00000000..f3040cdf --- /dev/null +++ b/WebApiClientCore.Benchmarks/User.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.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[] Utf8Array { get; } + + static User() + { + Utf8Array = File.ReadAllBytes("user.json"); + Instance = JsonSerializer.Deserialize(Utf8Array); + } + } +} diff --git a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj index fbfd41ac..5e6e3cdd 100644 --- a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj +++ b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj @@ -1,11 +1,11 @@  - net8.0 + netcoreapp3.1;net8.0 Exe true Sign.snk - false + false false @@ -19,4 +19,10 @@ + + + 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 d7ff935d..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.GetBenchmark-report-github.md +++ /dev/null @@ -1,14 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3) -Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 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 | 3.059 μs | 0.1315 μs | 0.3816 μs | 0.49 | 0.09 | 0.4959 | 2.03 KB | 0.51 | -| WebApiClientCore_GetAsync | 6.277 μs | 0.2695 μs | 0.7903 μs | 1.00 | 0.00 | 0.9766 | 4.02 KB | 1.00 | -| Refit_GetAsync | 14.295 μs | 0.4401 μs | 1.2626 μs | 2.30 | 0.31 | 1.2817 | 5.34 KB | 1.33 | 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 668c5b3f..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PostJsonBenchmark-report-github.md +++ /dev/null @@ -1,14 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3) -Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 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 | 3.804 μs | 0.1678 μs | 0.4813 μs | 0.60 | 0.11 | 0.5646 | 2.31 KB | 0.55 | -| WebApiClientCore_PostJsonAsync | 6.528 μs | 0.2695 μs | 0.7945 μs | 1.00 | 0.00 | 1.0147 | 4.17 KB | 1.00 | -| Refit_PostJsonAsync | 13.823 μs | 0.5368 μs | 1.5658 μs | 2.16 | 0.38 | 1.4038 | 5.83 KB | 1.40 | 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 b9f4ce95..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.PutFormBenchmark-report-github.md +++ /dev/null @@ -1,13 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3) -Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores - [Host] : .NET 8.0.4, X64 NativeAOT AVX2 - -Job=InProcess Toolchain=InProcessEmitToolchain - -``` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|------------------------------ |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_PutFormAsync | 10.45 μs | 0.415 μs | 1.204 μs | 1.00 | 0.00 | 1.2207 | 5 KB | 1.00 | -| Refit_PutFormAsync | 22.70 μs | 1.261 μs | 3.717 μs | 2.19 | 0.33 | 1.6785 | 6.89 KB | 1.38 | 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/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/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index 0db7e8c1..98f59ae5 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,11 +1,11 @@ using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using WebApiClientCore; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -87,22 +87,18 @@ public static void EnsureNotBuffered(this HttpContent httpContent) [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 ReadAsJsonAsync(this HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { -#if NET5_0_OR_GREATER - return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, options, cancellationToken).ConfigureAwait(false); -#else - var encoding = content.GetEncoding(); - if (encoding.Equals(Encoding.UTF8)) + var srcEncoding = content.GetEncoding(); + if (Encoding.UTF8.Equals(srcEncoding)) { - using var utf8Json = await content.ReadAsStreamAsync().ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(utf8Json, options, cancellationToken); + using var utf8Json = await content.ReadAsStreamCoreAsync(cancellationToken).ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(utf8Json, options, cancellationToken).ConfigureAwait(false); } else { - var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); - return utf8Json.Length == 0 ? default : JsonSerializer.Deserialize(utf8Json, options); + var byteArray = await content.ReadAsByteArrayCoreAsync(cancellationToken).ConfigureAwait(false); + var utf8Json = Encoding.Convert(srcEncoding, Encoding.UTF8, byteArray); + return JsonSerializer.Deserialize(utf8Json, options); } -#endif } /// @@ -117,22 +113,18 @@ public static void EnsureNotBuffered(this HttpContent httpContent) [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 ReadAsJsonAsync(this HttpContent content, Type objType, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { -#if NET5_0_OR_GREATER - return await System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync(content, objType, options, cancellationToken).ConfigureAwait(false); -#else - var encoding = content.GetEncoding(); - if (encoding.Equals(Encoding.UTF8)) + var srcEncoding = content.GetEncoding(); + if (Encoding.UTF8.Equals(srcEncoding)) { - using var stream = await content.ReadAsStreamAsync().ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(stream, objType, options, cancellationToken); + using var utf8Json = await content.ReadAsStreamCoreAsync(cancellationToken).ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(utf8Json, objType, options, cancellationToken).ConfigureAwait(false); } else { - var byteArray = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - var utf8Json = Encoding.Convert(encoding, Encoding.UTF8, byteArray); - return utf8Json.Length == 0 ? objType.DefaultValue() : JsonSerializer.Deserialize(utf8Json, objType, options); + var byteArray = await content.ReadAsByteArrayCoreAsync(cancellationToken).ConfigureAwait(false); + var utf8Json = Encoding.Convert(srcEncoding, Encoding.UTF8, byteArray); + return JsonSerializer.Deserialize(utf8Json, objType, options); } -#endif } /// @@ -146,18 +138,6 @@ public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent return httpContent.ReadAsByteArrayAsync(Encoding.UTF8, default); } - /// - /// 读取为二进制数组并转换为 utf8 编码 - /// - /// - /// - /// - /// - public static Task ReadAsUtf8ByteArrayAsync(this HttpContent httpContent, CancellationToken cancellationToken) - { - return httpContent.ReadAsByteArrayAsync(Encoding.UTF8, cancellationToken); - } - /// /// 读取为二进制数组并转换为指定的编码 /// @@ -181,14 +161,32 @@ public static Task ReadAsByteArrayAsync(this HttpContent httpContent, En public static async Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding, CancellationToken cancellationToken) { var encoding = httpContent.GetEncoding(); + var byteArray = await httpContent.ReadAsByteArrayCoreAsync(cancellationToken).ConfigureAwait(false); + return encoding.Equals(dstEncoding) ? byteArray : Encoding.Convert(encoding, dstEncoding, byteArray); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Task ReadAsByteArrayCoreAsync(this HttpContent httpContent, CancellationToken cancellationToken) + { #if NET5_0_OR_GREATER - var byteArray = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + return httpContent.ReadAsByteArrayAsync(cancellationToken); #else - var byteArray = await httpContent.ReadAsByteArrayAsync().ConfigureAwait(false); + return httpContent.ReadAsByteArrayAsync(); #endif - return encoding.Equals(dstEncoding) ? byteArray : Encoding.Convert(encoding, dstEncoding, byteArray); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Task ReadAsStreamCoreAsync(this HttpContent httpContent, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + return httpContent.ReadAsStreamAsync(cancellationToken); +#else + return httpContent.ReadAsStreamAsync(); +#endif + } + + /// /// 获取编码信息 /// From 359aee3ba26dacad893d0da9b63c74c588b8f192 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Wed, 19 Jun 2024 01:37:16 +0800 Subject: [PATCH 063/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0GET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Benchmarks/Program.cs | 3 ++- .../Requests/Benchmark.cs | 5 ---- .../Requests/HttpGetBenchmark.cs | 21 +++------------- .../Requests/HttpGetJsonBenchmark.cs | 25 +++++++++++++++++++ .../Requests/HttpPostJsonBenchmark.cs | 15 +---------- .../Requests/HttpPutFormBenchmark.cs | 4 +-- .../Requests/IRefitApi.cs | 5 +++- .../Requests/IWebApiClientCoreApi.cs | 4 +++ 8 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index 6dd0c705..2414eb2c 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -7,8 +7,9 @@ namespace WebApiClientCore.Benchmarks class Program { static void Main(string[] args) - { + { BenchmarkRunner.Run(); + BenchmarkRunner.Run(); BenchmarkRunner.Run(); BenchmarkRunner.Run(); Console.ReadLine(); diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 15af8824..7df955fb 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -21,11 +21,6 @@ public abstract class Benchmark public void GlobalSetup() { var services = new ServiceCollection(); - - services - .AddHttpClient(typeof(HttpClient).FullName) - .AddHttpMessageHandler(() => new UserResponseHandler()); - services .AddHttpApi(o => { diff --git a/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs index bf7f6031..fc98a45b 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs @@ -1,38 +1,25 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; -using System.Net.Http.Json; using System.Threading.Tasks; namespace WebApiClientCore.Benchmarks.Requests { public class HttpGetBenchmark : Benchmark { - [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 requestUri = $"http://webapiclient.com/{id}"; - return await httpClient.GetFromJsonAsync(requestUri); - } - [Benchmark(Baseline = true)] - public async Task WebApiClientCore_GetAsync() + public async Task WebApiClientCore_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await benchmarkApi.GetJsonAsync(id: "id"); + await benchmarkApi.GetAsync(id: "id001"); } [Benchmark] - public async Task Refit_GetAsync() + public async Task Refit_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await benchmarkApi.GetAsync(id: "id"); + 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..cec9af6a --- /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 index 60aef3ad..173294fc 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs @@ -1,24 +1,11 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; -using System.Net.Http.Json; using System.Threading.Tasks; namespace WebApiClientCore.Benchmarks.Requests { - [MemoryDiagnoser] public class HttpPostJsonBenchmark : Benchmark - { - [Benchmark] - public async Task HttpClient_PostJsonAsync() - { - using var scope = this.ServiceProvider.CreateScope(); - var httpClient = scope.ServiceProvider.GetRequiredService().CreateClient(typeof(HttpClient).FullName); - - var response = await httpClient.PostAsJsonAsync($"http://webapiclient.com/", User.Instance); - return await response.Content.ReadFromJsonAsync(); - } - + { [Benchmark(Baseline = true)] public async Task WebApiClientCore_PostJsonAsync() { diff --git a/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs index abf7435e..460c11fe 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs @@ -11,7 +11,7 @@ public async Task WebApiClientCore_PutFormAsync() { using var scope = this.ServiceProvider.CreateScope(); var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await benchmarkApi.PutFormAsync("id001", User.Instance); + return await benchmarkApi.PutFormAsync(id: "id001", User.Instance); } [Benchmark] @@ -19,7 +19,7 @@ public async Task Refit_PutFormAsync() { using var scope = this.ServiceProvider.CreateScope(); var benchmarkApi = scope.ServiceProvider.GetRequiredService(); - return await benchmarkApi.PutFormAsync("id001", User.Instance); + return await benchmarkApi.PutFormAsync(id: "id001", User.Instance); } } } diff --git a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs index c37e9613..5d4014bc 100644 --- a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs @@ -6,7 +6,10 @@ namespace WebApiClientCore.Benchmarks.Requests public interface IRefitApi { [Get("/benchmarks/{id}")] - Task GetAsync(string id); + Task GetAsync(string id); + + [Get("/benchmarks/{id}")] + Task GetJsonAsync(string id); [Post("/benchmarks")] Task PostJsonAsync([Body(BodySerializationMethod.Serialized)] User model); diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs index 7188d5e0..fdf49924 100644 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs @@ -4,8 +4,12 @@ namespace WebApiClientCore.Benchmarks.Requests { [JsonReturn] + [XmlReturn(Enable = false)] public interface IWebApiClientCoreApi { + [HttpGet("/benchmarks/{id}")] + Task GetAsync(string id); + [HttpGet("/benchmarks/{id}")] Task GetJsonAsync(string id); From 0f2705af9b2990e02d8e6a7934305ae6ebc30307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Wed, 19 Jun 2024 09:05:45 +0800 Subject: [PATCH 064/108] add xml --- .../Others/ReadAsJsonBenchamrk.cs | 6 +-- WebApiClientCore.Benchmarks/Program.cs | 1 + .../Requests/Benchmark.cs | 49 ++++++++++++++++--- .../Requests/HttpGetBenchmark.cs | 4 +- .../Requests/HttpGetJsonBenchmark.cs | 4 +- .../Requests/HttpPostJsonBenchmark.cs | 4 +- .../Requests/HttpPostXmlBenchmark.cs | 26 ++++++++++ .../Requests/HttpPutFormBenchmark.cs | 4 +- .../{IRefitApi.cs => IRefitJsonApi.cs} | 2 +- .../Requests/IRefitXmlApi.cs | 11 +++++ ...CoreApi.cs => IWebApiClientCoreJsonApi.cs} | 4 +- .../Requests/IWebApiClientCoreXmlApi.cs | 13 +++++ WebApiClientCore.Benchmarks/User.cs | 9 ++-- .../WebApiClientCore.Benchmarks.csproj | 6 ++- 14 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs rename WebApiClientCore.Benchmarks/Requests/{IRefitApi.cs => IRefitJsonApi.cs} (93%) create mode 100644 WebApiClientCore.Benchmarks/Requests/IRefitXmlApi.cs rename WebApiClientCore.Benchmarks/Requests/{IWebApiClientCoreApi.cs => IWebApiClientCoreJsonApi.cs} (84%) create mode 100644 WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs diff --git a/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs b/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs index e6ca538b..6370afd8 100644 --- a/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs +++ b/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs @@ -14,21 +14,21 @@ public class ReadAsJsonBenchmark [Benchmark(Baseline = true)] public async Task ReadAsJsonAsync() { - var content = new StreamContent(new MemoryStream(User.Utf8Array, writable: false)); + var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false)); return await content.ReadAsJsonAsync(null, default); } [Benchmark] public async Task ReadFromJsonAsync() { - var content = new StreamContent(new MemoryStream(User.Utf8Array, writable: false)); + var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false)); return await content.ReadFromJsonAsync(default(JsonSerializerOptions)); } [Benchmark] public async Task ReadAsByteArrayAsync() { - var content = new StreamContent(new MemoryStream(User.Utf8Array, writable: false)); + var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false)); var utf8Json = await content.ReadAsUtf8ByteArrayAsync(); return JsonSerializer.Deserialize(utf8Json, default(JsonSerializerOptions)); } diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index 2414eb2c..3f950e9b 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -10,6 +10,7 @@ static void Main(string[] args) { BenchmarkRunner.Run(); BenchmarkRunner.Run(); + BenchmarkRunner.Run(); BenchmarkRunner.Run(); BenchmarkRunner.Run(); Console.ReadLine(); diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 7df955fb..6efb7608 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -22,32 +23,49 @@ public void GlobalSetup() { var services = new ServiceCollection(); services - .AddHttpApi(o => + .AddHttpApi(o => { o.UseParameterPropertyValidate = false; o.UseReturnValuePropertyValidate = false; }) - .AddHttpMessageHandler(() => new UserResponseHandler()) + .AddHttpMessageHandler(() => new JsonResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services - .AddRefitClient(new RefitSettings + .AddHttpApi(o => { + o.UseParameterPropertyValidate = false; + o.UseReturnValuePropertyValidate = false; + }) + .AddHttpMessageHandler(() => new XmlResponseHandler()) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); + + services + .AddRefitClient(new RefitSettings + { + }) + .AddHttpMessageHandler(() => new JsonResponseHandler()) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); + + services + .AddRefitClient(new RefitSettings + { + ContentSerializer = new XmlContentSerializer() }) - .AddHttpMessageHandler(() => new UserResponseHandler()) + .AddHttpMessageHandler(() => new XmlResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); this.ServiceProvider = services.BuildServiceProvider(); - this.ServiceProvider.GetService(); - this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); } - private class UserResponseHandler : DelegatingHandler + 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.Utf8Array, writable: false), User.Utf8Array.Length); + var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false), User.Utf8Json.Length); content.Headers.ContentType = applicationJson; var response = new HttpResponseMessage(HttpStatusCode.OK) { @@ -56,5 +74,20 @@ protected override Task SendAsync(HttpRequestMessage reques return Task.FromResult(response); } } + + 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/HttpGetBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs index fc98a45b..79b2f4bf 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpGetBenchmark.cs @@ -10,7 +10,7 @@ public class HttpGetBenchmark : Benchmark public async Task WebApiClientCore_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); await benchmarkApi.GetAsync(id: "id001"); } @@ -18,7 +18,7 @@ public async Task WebApiClientCore_GetAsync() public async Task Refit_GetAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); await benchmarkApi.GetAsync(id: "id001"); } } diff --git a/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs index cec9af6a..45ccebb7 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpGetJsonBenchmark.cs @@ -10,7 +10,7 @@ public class HttpGetJsonBenchmark : Benchmark public async Task WebApiClientCore_GetJsonAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); return await benchmarkApi.GetJsonAsync(id: "id001"); } @@ -18,7 +18,7 @@ public async Task WebApiClientCore_GetJsonAsync() public async Task Refit_GetJsonAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + 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 index 173294fc..9ccaa97d 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostJsonBenchmark.cs @@ -10,7 +10,7 @@ public class HttpPostJsonBenchmark : Benchmark public async Task WebApiClientCore_PostJsonAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); return await benchmarkApi.PostJsonAsync(User.Instance); } @@ -19,7 +19,7 @@ public async Task WebApiClientCore_PostJsonAsync() public async Task Refit_PostJsonAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + 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..0790372f --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs @@ -0,0 +1,26 @@ +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 index 460c11fe..9672d6c1 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpPutFormBenchmark.cs @@ -10,7 +10,7 @@ public class HttpPutFormBenchmark : Benchmark public async Task WebApiClientCore_PutFormAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); return await benchmarkApi.PutFormAsync(id: "id001", User.Instance); } @@ -18,7 +18,7 @@ public async Task WebApiClientCore_PutFormAsync() public async Task Refit_PutFormAsync() { using var scope = this.ServiceProvider.CreateScope(); - var benchmarkApi = scope.ServiceProvider.GetRequiredService(); + 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/IRefitJsonApi.cs similarity index 93% rename from WebApiClientCore.Benchmarks/Requests/IRefitApi.cs rename to WebApiClientCore.Benchmarks/Requests/IRefitJsonApi.cs index 5d4014bc..2e094ec0 100644 --- a/WebApiClientCore.Benchmarks/Requests/IRefitApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IRefitJsonApi.cs @@ -3,7 +3,7 @@ namespace WebApiClientCore.Benchmarks.Requests { - public interface IRefitApi + public interface IRefitJsonApi { [Get("/benchmarks/{id}")] Task GetAsync(string id); 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/IWebApiClientCoreJsonApi.cs similarity index 84% rename from WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs rename to WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs index fdf49924..41d8a296 100644 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs @@ -5,13 +5,13 @@ namespace WebApiClientCore.Benchmarks.Requests { [JsonReturn] [XmlReturn(Enable = false)] - public interface IWebApiClientCoreApi + public interface IWebApiClientCoreJsonApi { [HttpGet("/benchmarks/{id}")] Task GetAsync(string id); [HttpGet("/benchmarks/{id}")] - Task GetJsonAsync(string id); + Task GetJsonAsync(string id); [HttpPost("/benchmarks")] Task PostJsonAsync([JsonContent] User model); diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs new file mode 100644 index 00000000..5be72a2d --- /dev/null +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using WebApiClientCore.Attributes; + +namespace WebApiClientCore.Benchmarks.Requests +{ + [XmlReturn] + [JsonReturn(Enable = false)] + public interface IWebApiClientCoreXmlApi + { + [HttpPost("/benchmarks")] + Task PostXmlAsync([XmlContent] User model); + } +} diff --git a/WebApiClientCore.Benchmarks/User.cs b/WebApiClientCore.Benchmarks/User.cs index f3040cdf..3f704601 100644 --- a/WebApiClientCore.Benchmarks/User.cs +++ b/WebApiClientCore.Benchmarks/User.cs @@ -1,6 +1,7 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using WebApiClientCore.Serialization; namespace WebApiClientCore.Benchmarks { @@ -26,12 +27,14 @@ public class User public static User Instance { get; } - public static byte[] Utf8Array { get; } + public static byte[] Utf8Json { get; } + public static string XmlString { get; set; } static User() { - Utf8Array = File.ReadAllBytes("user.json"); - Instance = JsonSerializer.Deserialize(Utf8Array); + 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 5e6e3cdd..fbe94c46 100644 --- a/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj +++ b/WebApiClientCore.Benchmarks/WebApiClientCore.Benchmarks.csproj @@ -1,17 +1,19 @@  - netcoreapp3.1;net8.0 + net8.0 Exe + true true Sign.snk - false + false false + From 7239c52cec80aa0d1c6bc9ddf6f2ab93f4fd1856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Wed, 19 Jun 2024 09:27:23 +0800 Subject: [PATCH 065/108] =?UTF-8?q?=E6=8F=90=E9=AB=98IsMatchAcceptContentT?= =?UTF-8?q?ype=E7=9A=84=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReturnAttributes/JsonReturnAttribute.cs | 20 ++++++------------- .../ReturnAttributes/XmlReturnAttribute.cs | 14 ++++++------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs index 3222b158..34e1f245 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/JsonReturnAttribute.cs @@ -1,8 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +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 { @@ -11,15 +12,7 @@ namespace WebApiClientCore.Attributes /// 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内容的结果特性 @@ -46,9 +39,8 @@ public JsonReturnAttribute(double acceptQuality) /// 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); } /// diff --git a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs index 49aa9aa8..ed0f31a9 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/XmlReturnAttribute.cs @@ -1,8 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +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 { @@ -11,10 +12,7 @@ namespace WebApiClientCore.Attributes /// 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内容的结果特性 @@ -41,8 +39,8 @@ public XmlReturnAttribute(double acceptQuality) /// 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); } /// From e05605958d7aaea88c4a6547f0b0dd98252507cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Wed, 19 Jun 2024 09:54:15 +0800 Subject: [PATCH 066/108] =?UTF-8?q?WebApiClient=E4=B8=8D=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E9=85=8D=E7=BD=AEReturnAttribute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Benchmarks/Program.cs | 2 -- .../Requests/Benchmark.cs | 16 +++++++++------- .../Requests/HttpPostXmlBenchmark.cs | 7 +++---- .../Requests/IWebApiClientCoreJsonApi.cs | 4 +--- .../Requests/IWebApiClientCoreXmlApi.cs | 4 +--- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index 3f950e9b..a616982e 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Running; -using System; using WebApiClientCore.Benchmarks.Requests; namespace WebApiClientCore.Benchmarks @@ -13,7 +12,6 @@ static void Main(string[] args) BenchmarkRunner.Run(); BenchmarkRunner.Run(); BenchmarkRunner.Run(); - Console.ReadLine(); } } } diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 6efb7608..2a2adec8 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -23,26 +23,28 @@ public void GlobalSetup() { var services = new ServiceCollection(); services - .AddHttpApi(o => + .AddWebApiClient() + .ConfigureHttpApi(o => { + o.UseLogging = false; o.UseParameterPropertyValidate = false; o.UseReturnValuePropertyValidate = false; - }) + }); + + services + .AddHttpApi() .AddHttpMessageHandler(() => new JsonResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services - .AddHttpApi(o => - { - o.UseParameterPropertyValidate = false; - o.UseReturnValuePropertyValidate = false; - }) + .AddHttpApi() .AddHttpMessageHandler(() => new XmlResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); services .AddRefitClient(new RefitSettings { + ContentSerializer = new SystemTextJsonContentSerializer() }) .AddHttpMessageHandler(() => new JsonResponseHandler()) .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); diff --git a/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs b/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs index 0790372f..f282158a 100644 --- a/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/HttpPostXmlBenchmark.cs @@ -5,21 +5,20 @@ 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(); + 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(); + var benchmarkApi = scope.ServiceProvider.GetRequiredService(); return await benchmarkApi.PostXmlAsync(User.Instance); } } diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs index 41d8a296..23bc02a4 100644 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreJsonApi.cs @@ -3,15 +3,13 @@ namespace WebApiClientCore.Benchmarks.Requests { - [JsonReturn] - [XmlReturn(Enable = false)] public interface IWebApiClientCoreJsonApi { [HttpGet("/benchmarks/{id}")] Task GetAsync(string id); [HttpGet("/benchmarks/{id}")] - Task GetJsonAsync(string id); + Task GetJsonAsync(string id); [HttpPost("/benchmarks")] Task PostJsonAsync([JsonContent] User model); diff --git a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs index 5be72a2d..46663372 100644 --- a/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs +++ b/WebApiClientCore.Benchmarks/Requests/IWebApiClientCoreXmlApi.cs @@ -3,11 +3,9 @@ namespace WebApiClientCore.Benchmarks.Requests { - [XmlReturn] - [JsonReturn(Enable = false)] public interface IWebApiClientCoreXmlApi { [HttpPost("/benchmarks")] - Task PostXmlAsync([XmlContent] User model); + Task PostXmlAsync([XmlContent] User model); } } From 1123291cafeb1b659877e2a6eeb1ce82d79565ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Wed, 19 Jun 2024 10:43:56 +0800 Subject: [PATCH 067/108] =?UTF-8?q?HttpContent=E4=BD=BF=E7=94=A8ReadAsXXXA?= =?UTF-8?q?sync=E5=90=8C=E5=90=8D=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiResponseContextExtensions.cs | 9 +---- .../LoggingFilterAttribute.cs | 2 +- .../ReturnAttributes/RawReturnAttribute.cs | 6 +-- .../HttpContentExtensions.cs | 34 +++++++++++++++++ .../System.Net.Http/HttpContentExtensions.cs | 37 ++++--------------- .../HttpResponseMessageExtensions.cs | 18 +-------- 6 files changed, 50 insertions(+), 56 deletions(-) create mode 100644 WebApiClientCore/BuildinExtensions/HttpContentExtensions.cs diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 6216a317..59302dd5 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -20,7 +20,7 @@ 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; if (response == null) { @@ -31,7 +31,7 @@ public static class ApiResponseContextExtensions var options = context.HttpContext.HttpApiOptions.JsonDeserializeOptions; return await content.ReadAsJsonAsync(objType, options, context.RequestAborted); } - + /// /// 使用Xml反序列化响应内容为目标类型 /// @@ -49,12 +49,7 @@ public static class ApiResponseContextExtensions var content = response.Content; var options = context.HttpContext.HttpApiOptions.XmlDeserializeOptions; - -#if NET5_0_OR_GREATER var xml = await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false); -#else - var xml = await content.ReadAsStringAsync().ConfigureAwait(false); -#endif return XmlSerializer.Deserialize(xml, objType, options); } } diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index 926ca42b..2151c69e 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -124,7 +124,7 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) if (content.IsBuffered() == true || context.GetCompletionOption() == HttpCompletionOption.ResponseContentRead) { - return await content.ReadAsStringAsync().ConfigureAwait(false); + return await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false); } return "..."; diff --git a/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs b/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs index 29230552..7d15df78 100644 --- a/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs +++ b/WebApiClientCore/Attributes/ReturnAttributes/RawReturnAttribute.cs @@ -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/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/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index 98f59ae5..6968d052 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,11 +1,11 @@ using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using WebApiClientCore; using WebApiClientCore.Exceptions; using WebApiClientCore.Internals; @@ -90,12 +90,12 @@ public static void EnsureNotBuffered(this HttpContent httpContent) var srcEncoding = content.GetEncoding(); if (Encoding.UTF8.Equals(srcEncoding)) { - using var utf8Json = await content.ReadAsStreamCoreAsync(cancellationToken).ConfigureAwait(false); + using var utf8Json = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync(utf8Json, options, cancellationToken).ConfigureAwait(false); } else { - var byteArray = await content.ReadAsByteArrayCoreAsync(cancellationToken).ConfigureAwait(false); + var byteArray = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); var utf8Json = Encoding.Convert(srcEncoding, Encoding.UTF8, byteArray); return JsonSerializer.Deserialize(utf8Json, options); } @@ -116,12 +116,12 @@ public static void EnsureNotBuffered(this HttpContent httpContent) var srcEncoding = content.GetEncoding(); if (Encoding.UTF8.Equals(srcEncoding)) { - using var utf8Json = await content.ReadAsStreamCoreAsync(cancellationToken).ConfigureAwait(false); + using var utf8Json = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync(utf8Json, objType, options, cancellationToken).ConfigureAwait(false); } else { - var byteArray = await content.ReadAsByteArrayCoreAsync(cancellationToken).ConfigureAwait(false); + var byteArray = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); var utf8Json = Encoding.Convert(srcEncoding, Encoding.UTF8, byteArray); return JsonSerializer.Deserialize(utf8Json, objType, options); } @@ -161,32 +161,10 @@ public static Task ReadAsByteArrayAsync(this HttpContent httpContent, En public static async Task ReadAsByteArrayAsync(this HttpContent httpContent, Encoding dstEncoding, CancellationToken cancellationToken) { var encoding = httpContent.GetEncoding(); - var byteArray = await httpContent.ReadAsByteArrayCoreAsync(cancellationToken).ConfigureAwait(false); + var byteArray = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); return encoding.Equals(dstEncoding) ? byteArray : Encoding.Convert(encoding, dstEncoding, byteArray); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Task ReadAsByteArrayCoreAsync(this HttpContent httpContent, CancellationToken cancellationToken) - { -#if NET5_0_OR_GREATER - return httpContent.ReadAsByteArrayAsync(cancellationToken); -#else - return httpContent.ReadAsByteArrayAsync(); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Task ReadAsStreamCoreAsync(this HttpContent httpContent, CancellationToken cancellationToken) - { -#if NET5_0_OR_GREATER - return httpContent.ReadAsStreamAsync(cancellationToken); -#else - return httpContent.ReadAsStreamAsync(); -#endif - } - - /// /// 获取编码信息 /// @@ -210,6 +188,7 @@ public static Encoding GetEncoding(this HttpContent httpContent) return encoding.Equals(Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) ? Encoding.UTF8 : Encoding.GetEncoding(encoding.ToString()); - } + } + } } diff --git a/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs b/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs index 2e37c1f2..f37423ec 100644 --- a/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpResponseMessageExtensions.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using WebApiClientCore; using WebApiClientCore.Internals; namespace System.Net.Http @@ -73,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); } @@ -104,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; @@ -124,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); } From 3a3d744957887d04d1d65cd8da1758fe4b62247b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Wed, 19 Jun 2024 13:17:25 +0800 Subject: [PATCH 068/108] =?UTF-8?q?=E8=B0=83=E6=95=B4HttpCompletionOption?= =?UTF-8?q?=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppAot/ICloudflareApi.cs | 3 +- .../Contexts/ApiRequestContext.cs | 6 ++-- .../ApiRequestContextExtensions.cs | 21 +------------- .../LoggingFilterAttribute.cs | 9 ++---- .../Implementations/ApiRequestSender.cs | 28 ++++++++++++++++++- 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/AppAot/ICloudflareApi.cs b/AppAot/ICloudflareApi.cs index 407465e8..3e24eb2d 100644 --- a/AppAot/ICloudflareApi.cs +++ b/AppAot/ICloudflareApi.cs @@ -1,11 +1,10 @@ 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")] diff --git a/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs b/WebApiClientCore.Abstractions/Contexts/ApiRequestContext.cs index 27679f15..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 @@ -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/ApiRequestContextExtensions.cs b/WebApiClientCore/ApiRequestContextExtensions.cs index eb6167cd..1fdbb42d 100644 --- a/WebApiClientCore/ApiRequestContextExtensions.cs +++ b/WebApiClientCore/ApiRequestContextExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using System; using System.Diagnostics.CodeAnalysis; -using System.Net.Http; namespace WebApiClientCore { @@ -78,25 +77,7 @@ public static bool TryGetArgument(this ApiRequestContext context, string paramet value = null; return false; - } - - /// - /// 返回请求使用的HttpCompletionOption - /// - /// - /// - internal static HttpCompletionOption GetCompletionOption(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; - } + } /// /// 获取指向 api 方法名的日志 diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index 2151c69e..345fb90e 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -122,12 +122,9 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) return null; } - if (content.IsBuffered() == true || context.GetCompletionOption() == HttpCompletionOption.ResponseContentRead) - { - return await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false); - } - - return "..."; + return content.IsBuffered() == true + ? await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false) + : "..."; } /// diff --git a/WebApiClientCore/Implementations/ApiRequestSender.cs b/WebApiClientCore/Implementations/ApiRequestSender.cs index de925545..1a36420e 100644 --- a/WebApiClientCore/Implementations/ApiRequestSender.cs +++ b/WebApiClientCore/Implementations/ApiRequestSender.cs @@ -54,7 +54,7 @@ private static async Task SendCoreAsync(ApiRequestContext context, CancellationT { var client = context.HttpContext.HttpClient; var request = context.HttpContext.RequestMessage; - var completionOption = context.GetCompletionOption(); + var completionOption = GetCompletionOption(context); var response = await client.SendAsync(request, completionOption, requestAborted).ConfigureAwait(false); context.HttpContext.ResponseMessage = response; @@ -62,6 +62,32 @@ private static async Task SendCoreAsync(ApiRequestContext context, CancellationT } } + + /// + /// 返回请求使用的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; + } + /// /// 获取响应的缓存 /// From f84f1a587c6456977eabcd02f86711c2e75e8ad1 Mon Sep 17 00:00:00 2001 From: Ezreal Date: Wed, 19 Jun 2024 23:20:10 +0800 Subject: [PATCH 069/108] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E5=9B=A0=E5=86=99=E6=B3=95?= =?UTF-8?q?=E5=92=8COS=E5=B7=AE=E5=BC=82=E5=9C=A8Ubuntu=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Attributes/ActionAttributes/HttpHostAttributeTest.cs | 2 +- WebApiClientCore.Test/Attributes/FormDataTextAttributeTest.cs | 4 +--- .../Implementations/HttpApiRequestMessageTest.cs | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) 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/Implementations/HttpApiRequestMessageTest.cs b/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs index 3880dc86..c321f93c 100644 --- a/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs +++ b/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs @@ -114,9 +114,7 @@ public async Task AddFormDataTextTest() { 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"}{HttpUtil.UrlEncode(value, Encoding.UTF8)}"; } var reqeust = new HttpApiRequestMessageImpl(); From b612cc3670089f9d9acc7a4a19f2e07cdb43f2ce Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Wed, 19 Jun 2024 23:56:29 +0800 Subject: [PATCH 070/108] =?UTF-8?q?HttpMethod=E6=97=A5=E5=BF=97=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParameterAttributes/HttpContentAttribute.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs index deb59788..95b3f8a0 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs @@ -11,7 +11,7 @@ namespace WebApiClientCore.Attributes /// public abstract class HttpContentAttribute : ApiParameterAttribute { - private static readonly Action logWarning = LoggerMessage.Define(LogLevel.Warning, 0, "{message}"); + private static readonly Action logWarning = LoggerMessage.Define(LogLevel.Warning, 0, Resx.gethead_Content_Warning); /// /// http请求之前 @@ -19,7 +19,7 @@ public abstract class HttpContentAttribute : ApiParameterAttribute /// 上下文 /// /// - public sealed override async Task OnRequestAsync(ApiParameterContext context) + public sealed override Task OnRequestAsync(ApiParameterContext context) { var method = context.HttpContext.RequestMessage.Method; if (method == HttpMethod.Get || method == HttpMethod.Head) @@ -27,11 +27,11 @@ public sealed override async Task OnRequestAsync(ApiParameterContext context) var logger = context.GetLogger(); if (logger != null) { - logWarning(logger, Resx.gethead_Content_Warning.Format(method), null); + logWarning(logger, method, null); } } - await this.SetHttpContentAsync(context).ConfigureAwait(false); + return this.SetHttpContentAsync(context); } /// From 85fc00c8d3a5d3ec6db9218aae8008f456172c19 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 20 Jun 2024 00:09:12 +0800 Subject: [PATCH 071/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=A9=BA=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 79b2d8f1..30874138 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -112,8 +112,8 @@ public override string ToString() 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 parametersString = string.Join(", ", method.Parameters.Select(item => $"{GetFullName(item.Type)} {item.Name}")); + var parameterNamesString = string.Join(", ", method.Parameters.Select(item => item.Name)); var parameterArrayString = string.IsNullOrEmpty(parameterNamesString) ? "global::System.Array.Empty()" : $"new global::System.Object[] {{ {parameterNamesString} }}"; From 0f29da42571aca7ec53bd8974c14108085a76b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 11:12:24 +0800 Subject: [PATCH 072/108] =?UTF-8?q?=E4=BC=98=E5=8C=96LoggingFilterAttribut?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/App.csproj | 4 + App/Program.cs | 1 + App/SerilogExtensions.cs | 28 ++++++ App/appsettings.json | 15 ++-- .../ApiActionDescriptor.cs | 2 +- .../ApiRequestContextExtensions.cs | 27 +++--- .../Attributes/FilterAttributes/LogMessage.cs | 70 ++++++++++++++- .../LoggingFilterAttribute.cs | 86 +++++++++---------- .../HttpContentAttribute.cs | 10 +-- .../DefaultApiActionDescriptor.cs | 4 +- 10 files changed, 176 insertions(+), 71 deletions(-) create mode 100644 App/SerilogExtensions.cs diff --git a/App/App.csproj b/App/App.csproj index 7bba3f9b..b10131b4 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -9,6 +9,10 @@ + + + + diff --git a/App/Program.cs b/App/Program.cs index ffd6de57..e90e61f7 100644 --- a/App/Program.cs +++ b/App/Program.cs @@ -26,6 +26,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) { return Host .CreateDefaultBuilder(args) + .UseSerilog(writeToProviders: false) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/App/SerilogExtensions.cs b/App/SerilogExtensions.cs new file mode 100644 index 00000000..9ac6c4a4 --- /dev/null +++ b/App/SerilogExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Serilog扩展 + /// + public static class SerilogExtensions + { + private const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}]{NewLine}{SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}"; + + /// + /// 使用Serilog + /// + /// + /// 是否同时写入到非Serilog的日志提供者 + public static IHostBuilder UseSerilog(this IHostBuilder builder, bool writeToProviders) + { + return builder.UseSerilog((context, logger) => + { + logger.ReadFrom.Configuration(context.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: template); + }, writeToProviders); + } + } +} diff --git a/App/appsettings.json b/App/appsettings.json index 9dc9c620..29ac0d7f 100644 --- a/App/appsettings.json +++ b/App/appsettings.json @@ -1,11 +1,14 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "MinimumLevel": { "Default": "Information", - "System": "Warning", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Override": { + "Default": "Information", + "System": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } } - }, + }, "AllowedHosts": "*" } diff --git a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs index 53a39feb..fafee9f0 100644 --- a/WebApiClientCore.Abstractions/ApiActionDescriptor.cs +++ b/WebApiClientCore.Abstractions/ApiActionDescriptor.cs @@ -57,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/ApiRequestContextExtensions.cs b/WebApiClientCore/ApiRequestContextExtensions.cs index 1fdbb42d..489b0ec5 100644 --- a/WebApiClientCore/ApiRequestContextExtensions.cs +++ b/WebApiClientCore/ApiRequestContextExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace WebApiClientCore { @@ -77,24 +78,30 @@ public static bool TryGetArgument(this ApiRequestContext context, string paramet value = null; return false; - } + } /// - /// 获取指向 api 方法名的日志 + /// 获取以 ApiAction 为容器的 ILogger /// /// /// - internal static ILogger? GetLogger(this ApiRequestContext context) + public static ILogger? GetActionLogger(this ApiRequestContext context) { - var loggerFactory = context.HttpContext.ServiceProvider.GetService(); - if (loggerFactory == null) + return context.ActionDescriptor.Properties.GetOrAdd(typeof(ILogger), CreateLogger) as ILogger; + + 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.Namespace}.{action.InterfaceType.Name}.{action.Member.Name}({string.Join(", ", parameters)})"; + return loggerFactory.CreateLogger(categoryName); + } } } } 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 345fb90e..8d8f6f57 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -11,8 +11,7 @@ namespace WebApiClientCore.Attributes /// public class LoggingFilterAttribute : ApiFilterAttribute { - private static readonly Action logError = LoggerMessage.Define(LogLevel.Error, 0, "{LogMessage}"); - private static readonly Action logInformation = LoggerMessage.Define(LogLevel.Information, 1, "{LogMessage}"); + private readonly bool isLoggingFilterAttribute; /// /// 获取或设置是否输出请求内容 @@ -30,6 +29,7 @@ public class LoggingFilterAttribute : ApiFilterAttribute public LoggingFilterAttribute() { this.OrderIndex = int.MaxValue; + this.isLoggingFilterAttribute = this.GetType() == typeof(LoggingFilterAttribute); } /// @@ -44,20 +44,26 @@ public sealed async override Task OnRequestAsync(ApiRequestContext context) return; } + // LoggingFilterAttribute本类型依赖于ILogger输出日志 + if (this.isLoggingFilterAttribute && context.GetActionLogger() == null) + { + 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); } /// @@ -67,12 +73,7 @@ public sealed async override Task OnRequestAsync(ApiRequestContext context) /// public sealed async override Task OnResponseAsync(ApiResponseContext context) { - if (context.HttpContext.HttpApiOptions.UseLogging == false) - { - return; - } - - var logMessage = context.Properties.Get(typeof(LoggingFilterAttribute)); + var logMessage = context.Properties.Get(typeof(LogMessage)); if (logMessage == null) { return; @@ -92,6 +93,33 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) 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); + } + /// /// 读取请求内容 /// @@ -122,43 +150,9 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) return null; } - return content.IsBuffered() == true - ? await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false) + return content.IsBuffered() == true + ? await content.ReadAsStringAsync(context.RequestAborted).ConfigureAwait(false) : "..."; } - - /// - /// 写日志到指定日志组件 - /// 默认写入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) - { - logInformation(logger, logMessage, null); - } - else - { - logError(logger, logMessage.ToExcludeException(), logMessage.Exception); - } - } } } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs index 95b3f8a0..471f81e6 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/HttpContentAttribute.cs @@ -11,7 +11,7 @@ namespace WebApiClientCore.Attributes /// public abstract class HttpContentAttribute : ApiParameterAttribute { - private static readonly Action logWarning = LoggerMessage.Define(LogLevel.Warning, 0, Resx.gethead_Content_Warning); + private static readonly Action logWarning = LoggerMessage.Define(LogLevel.Warning, 0, "{message}"); /// /// http请求之前 @@ -19,19 +19,19 @@ public abstract class HttpContentAttribute : ApiParameterAttribute /// 上下文 /// /// - public sealed override Task OnRequestAsync(ApiParameterContext context) + 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(); + var logger = context.GetActionLogger(); if (logger != null) { - logWarning(logger, method, null); + logWarning(logger, Resx.gethead_Content_Warning.Format(method), null); } } - return this.SetHttpContentAsync(context); + await this.SetHttpContentAsync(context).ConfigureAwait(false); } /// diff --git a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs index d9033bfc..80364bb9 100644 --- a/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs +++ b/WebApiClientCore/Implementations/DefaultApiActionDescriptor.cs @@ -61,7 +61,7 @@ public class DefaultApiActionDescriptor : ApiActionDescriptor /// /// 获取自定义数据存储的字典 /// - public override ConcurrentDictionary Properties { get; protected set; } + public override ConcurrentDictionary Properties { get; protected set; } /// /// 请求Api描述 @@ -109,7 +109,7 @@ public DefaultApiActionDescriptor( 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(); From aff4fc13e003a45af85d32dbc7b0e487e8bfc2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 13:04:52 +0800 Subject: [PATCH 073/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0IsLogEnable=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoggingFilterAttribute.cs | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index 8d8f6f57..b3f51b8a 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; @@ -44,8 +45,9 @@ public sealed async override Task OnRequestAsync(ApiRequestContext context) return; } - // LoggingFilterAttribute本类型依赖于ILogger输出日志 - if (this.isLoggingFilterAttribute && context.GetActionLogger() == null) + // 本类型依赖于 ActionLogger + // 且 LogLevel 最大为 LogLevel.Error + if (this.isLoggingFilterAttribute && !IsLogEnable(context)) { return; } @@ -79,18 +81,16 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) return; } - logMessage.ResponseTime = DateTime.Now; - logMessage.Exception = context.Exception; - - var response = context.HttpContext.ResponseMessage; - if (this.LogResponse && response != null) + if (this.isLoggingFilterAttribute && IsLogEnable(context, out var logger)) { - logMessage.HasResponse = true; - logMessage.ResponseHeaders = response.GetHeadersString(); - logMessage.ResponseContent = await ReadResponseContentAsync(context).ConfigureAwait(false); + 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); } - - await this.WriteLogAsync(context, logMessage).ConfigureAwait(false); } /// @@ -120,6 +120,44 @@ 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; + + var response = context.HttpContext.ResponseMessage; + if (this.LogResponse && response != null) + { + logMessage.HasResponse = true; + logMessage.ResponseHeaders = response.GetHeadersString(); + logMessage.ResponseContent = await ReadResponseContentAsync(context).ConfigureAwait(false); + } + } + /// /// 读取请求内容 /// From 2c8f64605266f839fd2e129eda87683e1ab8c3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 13:08:13 +0800 Subject: [PATCH 074/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8FullName?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/ApiRequestContextExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebApiClientCore/ApiRequestContextExtensions.cs b/WebApiClientCore/ApiRequestContextExtensions.cs index 489b0ec5..f9a3adfa 100644 --- a/WebApiClientCore/ApiRequestContextExtensions.cs +++ b/WebApiClientCore/ApiRequestContextExtensions.cs @@ -99,7 +99,7 @@ public static bool TryGetArgument(this ApiRequestContext context, string paramet var action = context.ActionDescriptor; var parameters = action.Parameters.Select(item => HttpApi.GetName(item.ParameterType, includeNamespace: false)); - var categoryName = $"{action.InterfaceType.Namespace}.{action.InterfaceType.Name}.{action.Member.Name}({string.Join(", ", parameters)})"; + var categoryName = $"{action.InterfaceType.FullName}.{action.Member.Name}({string.Join(", ", parameters)})"; return loggerFactory.CreateLogger(categoryName); } } From f203e4d0f89b4692fa10dc298c4cbbba3918c00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 13:31:08 +0800 Subject: [PATCH 075/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilterAttributes/LoggingFilterAttribute.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs index b3f51b8a..578d2a20 100644 --- a/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs +++ b/WebApiClientCore/Attributes/FilterAttributes/LoggingFilterAttribute.cs @@ -81,10 +81,13 @@ public sealed async override Task OnResponseAsync(ApiResponseContext context) return; } - if (this.isLoggingFilterAttribute && IsLogEnable(context, out var logger)) + if (this.isLoggingFilterAttribute) { - await this.FillResponseAsync(logMessage, context).ConfigureAwait(false); - logMessage.WriteTo(logger); + if (IsLogEnable(context, out var logger)) + { + await this.FillResponseAsync(logMessage, context).ConfigureAwait(false); + logMessage.WriteTo(logger); + } } else { From 809bbe7b9437a08b8caae6fb205223d32d1de7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 15:23:00 +0800 Subject: [PATCH 076/108] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Serialization/KeyValueSerializerOptions.cs | 7 ------- .../WebApiClientCore.Abstractions.csproj | 10 +++++----- WebApiClientCore/WebApiClientCore.csproj | 1 - 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs index 1862176a..4d2b7880 100644 --- a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs +++ b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs @@ -41,13 +41,8 @@ public JsonNamingPolicy? DictionaryKeyPolicy /// 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 } /// @@ -96,10 +91,8 @@ public KeyValueSerializerOptions() /// /// 目标类型 /// -#if NET8_0_OR_GREATER [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.")] -#endif 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 b0e1a9b8..69f7a0a9 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -1,10 +1,10 @@ - enable - netstandard2.1;net5.0;net8.0 + enable + netstandard2.1;net8.0 true - + WebApiClientCore WebApiClientCore.Abstractions True @@ -16,8 +16,8 @@ Sign.snk - - + + diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 67b4911d..e0816382 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -14,7 +14,6 @@ - From 6208a929c88329389d106697d7a68340d704000f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 16:23:32 +0800 Subject: [PATCH 077/108] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=9B=9E=E6=94=AF?= =?UTF-8?q?=E6=8C=81netcoreapp3.0=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/.config/dotnet-tools.json | 13 +++++++++ App/App.csproj | 7 +---- App/Program.cs | 3 +- App/SerilogExtensions.cs | 28 ------------------- App/Startup.cs | 3 +- App/appsettings.json | 15 ++++------ .../KeyValueSerializerOptions.cs | 5 ++++ .../WebApiClientCore.Abstractions.csproj | 4 +-- 8 files changed, 29 insertions(+), 49 deletions(-) create mode 100644 App/.config/dotnet-tools.json delete mode 100644 App/SerilogExtensions.cs 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/App.csproj b/App/App.csproj index b10131b4..91585e3f 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -3,20 +3,15 @@ Exe enable - net6.0 + netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 false false - - - - - diff --git a/App/Program.cs b/App/Program.cs index e90e61f7..0c0f82a7 100644 --- a/App/Program.cs +++ b/App/Program.cs @@ -25,8 +25,7 @@ public static void Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) { return Host - .CreateDefaultBuilder(args) - .UseSerilog(writeToProviders: false) + .CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/App/SerilogExtensions.cs b/App/SerilogExtensions.cs deleted file mode 100644 index 9ac6c4a4..00000000 --- a/App/SerilogExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Serilog; - -namespace Microsoft.Extensions.Hosting -{ - /// - /// Serilog扩展 - /// - public static class SerilogExtensions - { - private const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}]{NewLine}{SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}"; - - /// - /// 使用Serilog - /// - /// - /// 是否同时写入到非Serilog的日志提供者 - public static IHostBuilder UseSerilog(this IHostBuilder builder, bool writeToProviders) - { - return builder.UseSerilog((context, logger) => - { - logger.ReadFrom.Configuration(context.Configuration) - .Enrich.FromLogContext() - .WriteTo.Console(outputTemplate: template); - }, writeToProviders); - } - } -} diff --git a/App/Startup.cs b/App/Startup.cs index 55ba3c0b..0d0a8787 100644 --- a/App/Startup.cs +++ b/App/Startup.cs @@ -46,13 +46,12 @@ public void ConfigureServices(IServiceCollection services) // ȫĬ services - .AddWebApiClient() + .AddWebApiClient() .UseJsonFirstApiActionDescriptor(); // עuserApi services.AddHttpApi(typeof(IUserApi)).ConfigureHttpApi(o => { - o.UseLogging = Environment.IsDevelopment(); o.HttpHost = new Uri("http://localhost:5000/"); }); diff --git a/App/appsettings.json b/App/appsettings.json index 29ac0d7f..9dc9c620 100644 --- a/App/appsettings.json +++ b/App/appsettings.json @@ -1,14 +1,11 @@ { - "Serilog": { - "MinimumLevel": { + "Logging": { + "LogLevel": { "Default": "Information", - "Override": { - "Default": "Information", - "System": "Warning", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "System": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } - }, + }, "AllowedHosts": "*" } diff --git a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs index 4d2b7880..4342698b 100644 --- a/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs +++ b/WebApiClientCore.Abstractions/Serialization/KeyValueSerializerOptions.cs @@ -41,8 +41,13 @@ public JsonNamingPolicy? DictionaryKeyPolicy /// 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 } /// diff --git a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj index 69f7a0a9..db411581 100644 --- a/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj +++ b/WebApiClientCore.Abstractions/WebApiClientCore.Abstractions.csproj @@ -2,7 +2,7 @@ enable - netstandard2.1;net8.0 + netstandard2.1;net5.0;net8.0 true WebApiClientCore @@ -17,7 +17,7 @@ - + From bca74baa672257ad03f27b3337142b9d38eef2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 17:19:10 +0800 Subject: [PATCH 078/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A08.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Test/WebApiClientCore.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c45e3006ef0cd8017a13fbc4d29bdbf49c7bba2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 20 Jun 2024 17:40:47 +0800 Subject: [PATCH 079/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B5=AE=E7=82=B9?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- .../Serialization/KeyValueSerializerTest.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2f917f9b..cb1c6abf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.1.2 + 2.1.3 Copyright © laojiu 2017-2024 IDE0290;NETSDK1138 diff --git a/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs b/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs index 3de21360..a8adfd00 100644 --- a/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs +++ b/WebApiClientCore.Test/Serialization/KeyValueSerializerTest.cs @@ -18,34 +18,37 @@ public void SerializeTest() 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() { From a9f5e58bad3e705d2e668ffbd1d7ef433a0a08df Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 20 Jun 2024 22:33:26 +0800 Subject: [PATCH 080/108] =?UTF-8?q?=E5=87=8F=E5=B0=91MediaTypeHeaderValue?= =?UTF-8?q?=E7=9A=84=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/HttpContents/BufferContent.cs | 10 +++++++++- WebApiClientCore/HttpContents/FormContent.cs | 9 ++++++--- WebApiClientCore/HttpContents/JsonContent.cs | 12 ++++++++---- WebApiClientCore/HttpContents/JsonPatchContent.cs | 10 +++++++--- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/WebApiClientCore/HttpContents/BufferContent.cs b/WebApiClientCore/HttpContents/BufferContent.cs index a50232a8..13021c14 100644 --- a/WebApiClientCore/HttpContents/BufferContent.cs +++ b/WebApiClientCore/HttpContents/BufferContent.cs @@ -23,8 +23,16 @@ public class BufferContent : HttpContent, IBufferWriter /// utf8的BufferContent /// public BufferContent(string mediaType) + : this(new MediaTypeHeaderValue(mediaType)) { - this.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + } + + /// + /// utf8的BufferContent + /// + public BufferContent(MediaTypeHeaderValue mediaType) + { + this.Headers.ContentType = mediaType; } /// diff --git a/WebApiClientCore/HttpContents/FormContent.cs b/WebApiClientCore/HttpContents/FormContent.cs index 422b51de..3dc19c6a 100644 --- a/WebApiClientCore/HttpContents/FormContent.cs +++ b/WebApiClientCore/HttpContents/FormContent.cs @@ -18,6 +18,9 @@ namespace WebApiClientCore.HttpContents /// public class FormContent : HttpContent { + private const string mediaType = "application/x-www-form-urlencoded"; + private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new(mediaType); + /// /// buffer写入器 /// @@ -31,7 +34,7 @@ public class FormContent : HttpContent /// /// 获取对应的ContentType /// - public static string MediaType => "application/x-www-form-urlencoded"; + public static string MediaType => mediaType; /// @@ -39,7 +42,7 @@ public class FormContent : HttpContent /// public FormContent() { - this.Headers.ContentType = new MediaTypeHeaderValue(MediaType); + this.Headers.ContentType = mediaTypeHeaderValue; } /// @@ -47,9 +50,9 @@ public FormContent() /// /// 键值对 public FormContent(IEnumerable keyValues) + : this() { this.AddFormField(keyValues); - this.Headers.ContentType = new MediaTypeHeaderValue(MediaType); } /// diff --git a/WebApiClientCore/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index 16e9a45e..4fb03745 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Text; using System.Text.Json; using WebApiClientCore.Internals; @@ -11,16 +12,19 @@ namespace WebApiClientCore.HttpContents /// public class JsonContent : BufferContent { + private const string mediaType = "application/json"; + private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new(mediaType); + /// /// 获取对应的ContentType /// - public static string MediaType => "application/json"; + public static string MediaType => mediaType; /// /// uft8 的 json 内容 /// public JsonContent() - : base(MediaType) + : base(mediaTypeHeaderValue) { } @@ -45,7 +49,7 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) [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) + : base(mediaTypeHeaderValue) { if (encoding == null || Encoding.UTF8.Equals(encoding)) { @@ -57,7 +61,7 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions, 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 }; } } } diff --git a/WebApiClientCore/HttpContents/JsonPatchContent.cs b/WebApiClientCore/HttpContents/JsonPatchContent.cs index 4aa79725..1d15cd1f 100644 --- a/WebApiClientCore/HttpContents/JsonPatchContent.cs +++ b/WebApiClientCore/HttpContents/JsonPatchContent.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Text.Json; using WebApiClientCore.Serialization; @@ -10,16 +11,19 @@ namespace WebApiClientCore.HttpContents /// 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) { } @@ -31,7 +35,7 @@ public JsonPatchContent() [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(MediaType) + : base(mediaTypeHeaderValue) { JsonBufferSerializer.Serialize(this, operations, jsonSerializerOptions); } From fbe7cb78b74c064b4c5bbb1e2a4bf82497a0b223 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 20 Jun 2024 23:25:24 +0800 Subject: [PATCH 081/108] =?UTF-8?q?=E7=BC=93=E5=AD=98JsonBufferSerializer?= =?UTF-8?q?=E7=9A=84Utf8JsonWriter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Serialization/JsonBufferSerializer.cs | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/WebApiClientCore/Serialization/JsonBufferSerializer.cs b/WebApiClientCore/Serialization/JsonBufferSerializer.cs index d139dc03..6c5cf858 100644 --- a/WebApiClientCore/Serialization/JsonBufferSerializer.cs +++ b/WebApiClientCore/Serialization/JsonBufferSerializer.cs @@ -10,6 +10,9 @@ namespace WebApiClientCore.Serialization /// public static class JsonBufferSerializer { + [ThreadStatic] + private static Utf8JsonWriter? bufferWriterUtf8JsonWriter; + /// /// 默认选项 /// @@ -30,16 +33,40 @@ public static void Serialize(IBufferWriter bufferWriter, object? obj, Json return; } - var jsonOptions = options ?? defaultOptions; - var writerOptions = new JsonWriterOptions + options ??= defaultOptions; + var utf8JsonWriter = bufferWriterUtf8JsonWriter; + if (utf8JsonWriter == null) + { + utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); + bufferWriterUtf8JsonWriter = utf8JsonWriter; + } + else if (OptionsEquals(utf8JsonWriter.Options, options)) + { + utf8JsonWriter.Reset(bufferWriter); + } + else { + utf8JsonWriter.Dispose(); + utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); + bufferWriterUtf8JsonWriter = utf8JsonWriter; + } + + JsonSerializer.Serialize(utf8JsonWriter, obj, obj.GetType(), options); + } + + 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, - Encoder = jsonOptions.Encoder, - Indented = jsonOptions.WriteIndented }; - - using var utf8JsonWriter = new Utf8JsonWriter(bufferWriter, writerOptions); - JsonSerializer.Serialize(utf8JsonWriter, obj, obj.GetType(), jsonOptions); } } } \ No newline at end of file From b9a0fc9fade30ddc40e8cc56984c45c4190255f0 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 20 Jun 2024 23:41:29 +0800 Subject: [PATCH 082/108] =?UTF-8?q?=E5=85=B1=E4=BA=ABUtf8JsonWriterCache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Serialization/JsonBufferSerializer.cs | 39 +------------ .../Serialization/KeyValueSerializer.cs | 12 +--- .../Serialization/Utf8JsonWriterCache.cs | 58 +++++++++++++++++++ 3 files changed, 63 insertions(+), 46 deletions(-) create mode 100644 WebApiClientCore/Serialization/Utf8JsonWriterCache.cs diff --git a/WebApiClientCore/Serialization/JsonBufferSerializer.cs b/WebApiClientCore/Serialization/JsonBufferSerializer.cs index 6c5cf858..9ceb707a 100644 --- a/WebApiClientCore/Serialization/JsonBufferSerializer.cs +++ b/WebApiClientCore/Serialization/JsonBufferSerializer.cs @@ -1,5 +1,4 @@ -using System; -using System.Buffers; +using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Text.Json; @@ -10,9 +9,6 @@ namespace WebApiClientCore.Serialization /// public static class JsonBufferSerializer { - [ThreadStatic] - private static Utf8JsonWriter? bufferWriterUtf8JsonWriter; - /// /// 默认选项 /// @@ -34,39 +30,8 @@ public static void Serialize(IBufferWriter bufferWriter, object? obj, Json } options ??= defaultOptions; - var utf8JsonWriter = bufferWriterUtf8JsonWriter; - if (utf8JsonWriter == null) - { - utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); - bufferWriterUtf8JsonWriter = utf8JsonWriter; - } - else if (OptionsEquals(utf8JsonWriter.Options, options)) - { - utf8JsonWriter.Reset(bufferWriter); - } - else - { - utf8JsonWriter.Dispose(); - utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); - bufferWriterUtf8JsonWriter = utf8JsonWriter; - } - + var utf8JsonWriter = Utf8JsonWriterCache.Get(bufferWriter, options); JsonSerializer.Serialize(utf8JsonWriter, obj, obj.GetType(), options); } - - 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, - }; - } } } \ No newline at end of file diff --git a/WebApiClientCore/Serialization/KeyValueSerializer.cs b/WebApiClientCore/Serialization/KeyValueSerializer.cs index d9fe1b28..d159e048 100644 --- a/WebApiClientCore/Serialization/KeyValueSerializer.cs +++ b/WebApiClientCore/Serialization/KeyValueSerializer.cs @@ -80,16 +80,10 @@ public static IList Serialize(string key, object? obj, KeyValueSeriali [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, diff --git a/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs b/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs new file mode 100644 index 00000000..40f8203e --- /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? bufferWriterUtf8JsonWriter; + + /// + /// 获取与 bufferWriter 关联的 Utf8JsonWriter + /// + /// + /// + /// + public static Utf8JsonWriter Get(IBufferWriter bufferWriter, JsonSerializerOptions options) + { + var utf8JsonWriter = bufferWriterUtf8JsonWriter; + if (utf8JsonWriter == null) + { + utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); + bufferWriterUtf8JsonWriter = utf8JsonWriter; + } + else if (OptionsEquals(utf8JsonWriter.Options, options)) + { + utf8JsonWriter.Reset(bufferWriter); + } + else + { + utf8JsonWriter.Dispose(); + utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); + bufferWriterUtf8JsonWriter = 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, + }; + } + } +} From 655d4f936e7f9ce72dcb0ad54ce95a54deb1dfb5 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Thu, 20 Jun 2024 23:46:37 +0800 Subject: [PATCH 083/108] =?UTF-8?q?=E5=AD=97=E6=AE=B5=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/Serialization/Utf8JsonWriterCache.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs b/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs index 40f8203e..2ec8063a 100644 --- a/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs +++ b/WebApiClientCore/Serialization/Utf8JsonWriterCache.cs @@ -10,7 +10,7 @@ namespace WebApiClientCore.Serialization static class Utf8JsonWriterCache { [ThreadStatic] - private static Utf8JsonWriter? bufferWriterUtf8JsonWriter; + private static Utf8JsonWriter? threadUtf8JsonWriter; /// /// 获取与 bufferWriter 关联的 Utf8JsonWriter @@ -20,11 +20,11 @@ static class Utf8JsonWriterCache /// public static Utf8JsonWriter Get(IBufferWriter bufferWriter, JsonSerializerOptions options) { - var utf8JsonWriter = bufferWriterUtf8JsonWriter; + var utf8JsonWriter = threadUtf8JsonWriter; if (utf8JsonWriter == null) { utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); - bufferWriterUtf8JsonWriter = utf8JsonWriter; + threadUtf8JsonWriter = utf8JsonWriter; } else if (OptionsEquals(utf8JsonWriter.Options, options)) { @@ -34,7 +34,7 @@ public static Utf8JsonWriter Get(IBufferWriter bufferWriter, JsonSerialize { utf8JsonWriter.Dispose(); utf8JsonWriter = new Utf8JsonWriter(bufferWriter, GetJsonWriterOptions(options)); - bufferWriterUtf8JsonWriter = utf8JsonWriter; + threadUtf8JsonWriter = utf8JsonWriter; } return utf8JsonWriter; } From 748628be3dbb81810dda2cda572e10017c96a2aa Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 01:09:28 +0800 Subject: [PATCH 084/108] =?UTF-8?q?=E6=8F=90=E9=AB=98JsonContentAttribute?= =?UTF-8?q?=E7=9A=84=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JsonContentAttribute.cs | 25 +++++++++++++++++-- .../HttpContents/BufferContent.cs | 13 +++++++--- WebApiClientCore/HttpContents/JsonContent.cs | 9 +++---- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs index 83cda5bb..292f5b38 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using WebApiClientCore.HttpContents; @@ -36,9 +37,29 @@ public string CharSet [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] protected override Task SetHttpContentAsync(ApiParameterContext context) { - var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = new JsonContent(context.ParameterValue, options, this.encoding); + context.HttpContext.RequestMessage.Content = this.CreateContent(context); return Task.CompletedTask; } + + [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.")] +#if NET5_0_OR_GREATER + private System.Net.Http.Json.JsonContent CreateContent(ApiParameterContext context) + { + var value = context.ParameterValue; + var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; + var valueType = value == null ? context.Parameter.ParameterType : value.GetType(); + var mediaType = Encoding.UTF8.Equals(this.encoding) ? defaultMediaType : new MediaTypeHeaderValue(JsonContent.MediaType) { CharSet = this.CharSet }; + return System.Net.Http.Json.JsonContent.Create(value, valueType, mediaType, options); + } + private static readonly MediaTypeHeaderValue defaultMediaType = new(JsonContent.MediaType); +#else + private JsonContent CreateContent(ApiParameterContext context) + { + var value = context.ParameterValue; + var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; + return new JsonContent(value, options, this.encoding); + } +#endif } } diff --git a/WebApiClientCore/HttpContents/BufferContent.cs b/WebApiClientCore/HttpContents/BufferContent.cs index 13021c14..0aec2522 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,7 +20,7 @@ public class BufferContent : HttpContent, IBufferWriter private readonly RecyclableBufferWriter bufferWriter = new(); /// - /// utf8的BufferContent + /// 缓冲的 Http 内容 /// public BufferContent(string mediaType) : this(new MediaTypeHeaderValue(mediaType)) @@ -28,13 +28,20 @@ public BufferContent(string mediaType) } /// - /// utf8的BufferContent + /// 缓冲的 Http 内容 /// public BufferContent(MediaTypeHeaderValue mediaType) { this.Headers.ContentType = mediaType; } + /// + /// 缓冲的 Http 内容 + /// + protected BufferContent() + { + } + /// /// 设置向前推进实际写入的数据长度 /// diff --git a/WebApiClientCore/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index 4fb03745..ac55f628 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -13,7 +13,7 @@ namespace WebApiClientCore.HttpContents public class JsonContent : BufferContent { private const string mediaType = "application/json"; - private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new(mediaType); + private static readonly MediaTypeHeaderValue defaultMediaType = new(mediaType); /// /// 获取对应的ContentType @@ -24,7 +24,7 @@ public class JsonContent : BufferContent /// uft8 的 json 内容 /// public JsonContent() - : base(mediaTypeHeaderValue) + : base(defaultMediaType) { } @@ -49,19 +49,18 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) [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(mediaTypeHeaderValue) { if (encoding == null || Encoding.UTF8.Equals(encoding)) { + this.Headers.ContentType = defaultMediaType; JsonBufferSerializer.Serialize(this, value, jsonSerializerOptions); } else { + this.Headers.ContentType = new MediaTypeHeaderValue(mediaType) { CharSet = encoding.WebName }; using var utf8Writer = new RecyclableBufferWriter(); JsonBufferSerializer.Serialize(utf8Writer, value, jsonSerializerOptions); - Encoding.UTF8.Convert(encoding, utf8Writer.WrittenSpan, this); - this.Headers.ContentType = new MediaTypeHeaderValue(mediaType) { CharSet = encoding.WebName }; } } } From 74400a20744b100752f13bd96d58e08214ef77d5 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 01:33:04 +0800 Subject: [PATCH 085/108] add using --- WebApiClientCore/Serialization/XmlSerializer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index ee174f67..d0bc73f8 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -29,7 +29,7 @@ public static class XmlSerializer } var settings = options ?? writerSettings; - var writer = new EncodingWriter(settings.Encoding); + using var writer = new EncodingStingWriter(settings.Encoding); using var xmlWriter = XmlWriter.Create(writer, settings); var xmlSerializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); xmlSerializer.Serialize(xmlWriter, obj); @@ -66,7 +66,7 @@ public static class XmlSerializer /// /// 表示可指定编码文本写入器 /// - private class EncodingWriter : StringWriter + private class EncodingStingWriter : StringWriter { /// /// 编码 @@ -82,7 +82,7 @@ private class EncodingWriter : StringWriter /// 可指定编码文本写入器 /// /// 编码 - public EncodingWriter(Encoding encoding) + public EncodingStingWriter(Encoding encoding) { this.encoding = encoding; } From 6e222263397a97cbb4d1d4097547beb2b7806c6f Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 03:17:41 +0800 Subject: [PATCH 086/108] =?UTF-8?q?xml=E5=BA=8F=E5=88=97=E5=8C=96=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XmlContentAttributeTest.cs | 13 +- .../XmlContentAttribute.cs | 10 +- .../BuildinExtensions/EncodingExtensions.cs | 2 +- .../EncodingExtensions.netstd.cs | 186 ++++++++++++++++++ WebApiClientCore/HttpContents/XmlContent.cs | 27 ++- .../Serialization/XmlSerializer.cs | 137 ++++++++++++- 6 files changed, 354 insertions(+), 21 deletions(-) create mode 100644 WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs diff --git a/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs b/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs index bb820300..6e7d5d4b 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,14 @@ 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); + + using var bufferWriter = new RecyclableBufferWriter(); + XmlSerializer.Serialize(bufferWriter, context.Arguments[0], null); + var target = Encoding.GetEncoding(attr.CharSet).GetString(bufferWriter.WrittenSpan); Assert.True(body == target); } } diff --git a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs index 14ff92fd..37d85a8b 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs @@ -34,8 +34,14 @@ public string CharSet [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); + var options = context.HttpContext.HttpApiOptions.XmlSerializeOptions; + if (encoding != null && encoding.Equals(options.Encoding) == false) + { + options = options.Clone(); + options.Encoding = encoding; + } + + context.HttpContext.RequestMessage.Content = new XmlContent(context.ParameterValue, options); return Task.CompletedTask; } } diff --git a/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs b/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs index e7ef3fd1..2f4109fb 100644 --- a/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs +++ b/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs @@ -8,7 +8,7 @@ namespace WebApiClientCore /// /// 提供Encoding扩展 /// - static class EncodingExtensions + static partial class EncodingExtensions { /// /// 转换编码 diff --git a/WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs b/WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs new file mode 100644 index 00000000..976d18f4 --- /dev/null +++ b/WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs @@ -0,0 +1,186 @@ +#if NETSTANDARD2_1 +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace WebApiClientCore +{ + /// + /// 提供Encoding扩展 + /// + static partial class EncodingExtensions + { + /// + /// The maximum number of input elements after which we'll begin to chunk the input. + /// + /// + /// 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) + { + 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); + + writer.Advance(actualBytesWritten); + return actualBytesWritten; + } + else + { + // Allocate a stateful Encoder instance and chunk this. + + Convert(encoding.GetEncoder(), chars, writer, flush: true, out long totalBytesWritten, out _); + return totalBytesWritten; + } + } + + /// + /// 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; + } + else + { + // 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/HttpContents/XmlContent.cs b/WebApiClientCore/HttpContents/XmlContent.cs index 2fbac101..f0a525df 100644 --- a/WebApiClientCore/HttpContents/XmlContent.cs +++ b/WebApiClientCore/HttpContents/XmlContent.cs @@ -1,13 +1,18 @@ -using System.Net.Http; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; using System.Text; +using System.Xml; +using WebApiClientCore.Serialization; namespace WebApiClientCore.HttpContents { /// /// 表示 xml 内容 /// - public class XmlContent : StringContent + public class XmlContent : BufferContent { + private static readonly MediaTypeHeaderValue defaultMediaType = new(MediaType) { CharSet = Encoding.UTF8.WebName }; + /// /// 获取对应的ContentType /// @@ -19,8 +24,22 @@ public class XmlContent : StringContent /// xml内容 /// 编码 public XmlContent(string? xml, Encoding encoding) - : base(xml ?? string.Empty, encoding, MediaType) { + encoding.GetBytes(xml, this); + this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; + } + + /// + /// xml内容 + /// + /// xml实体 + /// xml写入设置项 + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] + public XmlContent(object? obj, XmlWriterSettings xmlWriterSettings) + { + XmlSerializer.Serialize(this, obj, xmlWriterSettings); + var encoding = xmlWriterSettings.Encoding; + this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; } } -} +} \ No newline at end of file diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index d0bc73f8..30db5688 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -1,7 +1,10 @@ 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; namespace WebApiClientCore.Serialization @@ -29,13 +32,34 @@ public static class XmlSerializer } var settings = options ?? writerSettings; - using var writer = new EncodingStingWriter(settings.Encoding); + using var writer = new EncodingStringWriter(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文本 + /// + /// buffer写入器 + /// 对象 + /// 配置选项 + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] + public static void Serialize(IBufferWriter bufferWriter, object? obj, XmlWriterSettings? options) + { + if (obj == null) + { + return; + } + + var settings = options ?? writerSettings; + using var writer = new BufferTextWriter(bufferWriter, settings.Encoding); + using var xmlWriter = XmlWriter.Create(writer, settings); + var xmlSerializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); + xmlSerializer.Serialize(xmlWriter, obj); + } + /// /// 将Xml文本反序列化对象 /// @@ -63,28 +87,121 @@ public static class XmlSerializer return xmlSerializer.Deserialize(xmlReader); } + + private class BufferTextWriter : TextWriter + { + private readonly IBufferWriter bufferWriter; + + public override Encoding Encoding { get; } + + public BufferTextWriter(IBufferWriter bufferWriter, Encoding encoding) + { + this.bufferWriter = bufferWriter; + this.Encoding = encoding; + } + public override Task FlushAsync() + { + return Task.CompletedTask; + } + + public override void Write(ReadOnlySpan buffer) + { + this.Encoding.GetBytes(buffer, this.bufferWriter); + } + + 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; + } + } + + /// /// 表示可指定编码文本写入器 /// - private class EncodingStingWriter : StringWriter + private class EncodingStringWriter : StringWriter { - /// - /// 编码 - /// - private readonly Encoding encoding; - /// /// 获取编码 /// - public override Encoding Encoding => this.encoding; + public override Encoding Encoding { get; } /// /// 可指定编码文本写入器 /// /// 编码 - public EncodingStingWriter(Encoding encoding) + public EncodingStringWriter(Encoding encoding) { - this.encoding = encoding; + this.Encoding = encoding; } } } From 567f8d7944c692c09fd3b01d1a2e5e4ad96882d8 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 04:04:14 +0800 Subject: [PATCH 087/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8System.Net.Http.Json?= =?UTF-8?q?=E5=A4=84=E7=90=86json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpApiRequestMessage.cs | 4 +- .../Others/ReadAsJsonBenchamrk.cs | 36 ----------- .../TokenProviders/OAuth2TokenClient.cs | 3 +- .../ApiResponseContextExtensions.cs | 4 +- .../JsonContentAttribute.cs | 37 ++++------- .../System.Net.Http/HttpContentExtensions.cs | 61 +------------------ WebApiClientCore/WebApiClientCore.csproj | 4 ++ 7 files changed, 25 insertions(+), 124 deletions(-) delete mode 100644 WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs diff --git a/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs b/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs index 51b50650..20e58ce0 100644 --- a/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs +++ b/WebApiClientCore.Abstractions/HttpApiRequestMessage.cs @@ -34,10 +34,10 @@ public abstract class HttpApiRequestMessage : HttpRequestMessage /// 名称 /// 值 /// - 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)); } /// diff --git a/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs b/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs deleted file mode 100644 index 6370afd8..00000000 --- a/WebApiClientCore.Benchmarks/Others/ReadAsJsonBenchamrk.cs +++ /dev/null @@ -1,36 +0,0 @@ -using BenchmarkDotNet.Attributes; -using System.IO; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading.Tasks; - -namespace WebApiClientCore.Benchmarks.Others -{ - [InProcess] - [MemoryDiagnoser] - public class ReadAsJsonBenchmark - { - [Benchmark(Baseline = true)] - public async Task ReadAsJsonAsync() - { - var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false)); - return await content.ReadAsJsonAsync(null, default); - } - - [Benchmark] - public async Task ReadFromJsonAsync() - { - var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false)); - return await content.ReadFromJsonAsync(default(JsonSerializerOptions)); - } - - [Benchmark] - public async Task ReadAsByteArrayAsync() - { - var content = new StreamContent(new MemoryStream(User.Utf8Json, writable: false)); - var utf8Json = await content.ReadAsUtf8ByteArrayAsync(); - return JsonSerializer.Deserialize(utf8Json, default(JsonSerializerOptions)); - } - } -} diff --git a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs index f5b4d3d8..b0a81167 100644 --- a/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs +++ b/WebApiClientCore.Extensions.OAuths/TokenProviders/OAuth2TokenClient.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Net.Http; +using System.Net.Http.Json; using System.Threading.Tasks; using WebApiClientCore.Attributes; using WebApiClientCore.HttpContents; @@ -90,7 +91,7 @@ public OAuth2TokenClient(IHttpClientFactory httpClientFactory, IOptionsMonitor(this.httpApiOptions.JsonDeserializeOptions); + return await response.Content.ReadFromJsonAsync(this.httpApiOptions.JsonDeserializeOptions); } } } diff --git a/WebApiClientCore/ApiResponseContextExtensions.cs b/WebApiClientCore/ApiResponseContextExtensions.cs index 59302dd5..714ef208 100644 --- a/WebApiClientCore/ApiResponseContextExtensions.cs +++ b/WebApiClientCore/ApiResponseContextExtensions.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Net.Http; +using System.Net.Http.Json; using System.Threading.Tasks; using WebApiClientCore.Serialization; @@ -29,7 +29,7 @@ public static class ApiResponseContextExtensions var content = response.Content; var options = context.HttpContext.HttpApiOptions.JsonDeserializeOptions; - return await content.ReadAsJsonAsync(objType, options, context.RequestAborted); + return await content.ReadFromJsonAsync(objType, options, context.RequestAborted); } /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs index 292f5b38..d723eed0 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -1,9 +1,9 @@ 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 { @@ -13,9 +13,10 @@ namespace WebApiClientCore.Attributes /// public class JsonContentAttribute : HttpContentAttribute, ICharSetable { - /// - /// 编码方式 - /// + private const string jsonMediaType = "application/json"; + private static readonly MediaTypeHeaderValue defaultMediaType = new(jsonMediaType); + + private MediaTypeHeaderValue mediaType = defaultMediaType; private Encoding encoding = Encoding.UTF8; /// @@ -25,7 +26,11 @@ 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 }; + } } /// @@ -36,30 +41,12 @@ public string CharSet [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) - { - context.HttpContext.RequestMessage.Content = this.CreateContent(context); - return Task.CompletedTask; - } - - [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.")] -#if NET5_0_OR_GREATER - private System.Net.Http.Json.JsonContent CreateContent(ApiParameterContext context) { var value = context.ParameterValue; - var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; var valueType = value == null ? context.Parameter.ParameterType : value.GetType(); - var mediaType = Encoding.UTF8.Equals(this.encoding) ? defaultMediaType : new MediaTypeHeaderValue(JsonContent.MediaType) { CharSet = this.CharSet }; - return System.Net.Http.Json.JsonContent.Create(value, valueType, mediaType, options); - } - private static readonly MediaTypeHeaderValue defaultMediaType = new(JsonContent.MediaType); -#else - private JsonContent CreateContent(ApiParameterContext context) - { - var value = context.ParameterValue; var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - return new JsonContent(value, options, this.encoding); + context.HttpContext.RequestMessage.Content = JsonContent.Create(value, valueType, this.mediaType, options); + return Task.CompletedTask; } -#endif } } diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index 6968d052..e1c7050e 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,8 +1,6 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using WebApiClientCore; @@ -72,61 +70,8 @@ public static void EnsureNotBuffered(this HttpContent httpContent) { throw new HttpContentBufferedException(); } - } - - - /// - /// 读取 json 内容为指定的类型 - /// - /// 目标类型 - /// http内容 - /// 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 async Task ReadAsJsonAsync(this HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken = default) - { - var srcEncoding = content.GetEncoding(); - if (Encoding.UTF8.Equals(srcEncoding)) - { - using var utf8Json = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(utf8Json, options, cancellationToken).ConfigureAwait(false); - } - else - { - var byteArray = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); - var utf8Json = Encoding.Convert(srcEncoding, Encoding.UTF8, byteArray); - return JsonSerializer.Deserialize(utf8Json, options); - } - } - - /// - /// 读取 json 内容为指定的类型 - /// - /// http内容 - /// 目标类型 - /// 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 async Task ReadAsJsonAsync(this HttpContent content, Type objType, JsonSerializerOptions? options, CancellationToken cancellationToken = default) - { - var srcEncoding = content.GetEncoding(); - if (Encoding.UTF8.Equals(srcEncoding)) - { - using var utf8Json = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(utf8Json, objType, options, cancellationToken).ConfigureAwait(false); - } - else - { - var byteArray = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); - var utf8Json = Encoding.Convert(srcEncoding, Encoding.UTF8, byteArray); - return JsonSerializer.Deserialize(utf8Json, objType, options); - } - } - + } + /// /// 读取为二进制数组并转换为 utf8 编码 /// diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index e0816382..11cc7796 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -19,6 +19,10 @@ + + + + From ecea0bd2a53f36aab606b47389a597d59a3e1a0b Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 04:41:41 +0800 Subject: [PATCH 088/108] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=BD=AC=E7=A0=81?= =?UTF-8?q?=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuildinExtensions/EncodingExtensions.cs | 189 +++++++++++++++--- .../EncodingExtensions.netstd.cs | 186 ----------------- .../HttpContents/BufferContent.cs | 13 ++ WebApiClientCore/HttpContents/JsonContent.cs | 13 +- 4 files changed, 183 insertions(+), 218 deletions(-) delete mode 100644 WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs diff --git a/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs b/WebApiClientCore/BuildinExtensions/EncodingExtensions.cs index 2f4109fb..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 @@ -8,42 +8,177 @@ namespace WebApiClientCore /// /// 提供Encoding扩展 /// - static partial class EncodingExtensions + 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()[..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/EncodingExtensions.netstd.cs b/WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs deleted file mode 100644 index 976d18f4..00000000 --- a/WebApiClientCore/BuildinExtensions/EncodingExtensions.netstd.cs +++ /dev/null @@ -1,186 +0,0 @@ -#if NETSTANDARD2_1 -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; - -namespace WebApiClientCore -{ - /// - /// 提供Encoding扩展 - /// - static partial class EncodingExtensions - { - /// - /// The maximum number of input elements after which we'll begin to chunk the input. - /// - /// - /// 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) - { - 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); - - writer.Advance(actualBytesWritten); - return actualBytesWritten; - } - else - { - // Allocate a stateful Encoder instance and chunk this. - - Convert(encoding.GetEncoder(), chars, writer, flush: true, out long totalBytesWritten, out _); - return totalBytesWritten; - } - } - - /// - /// 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; - } - else - { - // 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/HttpContents/BufferContent.cs b/WebApiClientCore/HttpContents/BufferContent.cs index 0aec2522..edc183ba 100644 --- a/WebApiClientCore/HttpContents/BufferContent.cs +++ b/WebApiClientCore/HttpContents/BufferContent.cs @@ -19,6 +19,11 @@ public class BufferContent : HttpContent, IBufferWriter /// private readonly RecyclableBufferWriter bufferWriter = new(); + /// + /// 获取已数入的数据 + /// + protected ArraySegment WrittenSegment => this.bufferWriter.WrittenSegment; + /// /// 缓冲的 Http 内容 /// @@ -93,6 +98,14 @@ public void Write(Span buffer) this.bufferWriter.Write(buffer); } + /// + /// 清除数据 + /// + public void Clear() + { + this.bufferWriter.Clear(); + } + /// /// 创建只读流 /// diff --git a/WebApiClientCore/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index ac55f628..bc5a4aa6 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -2,7 +2,6 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; -using WebApiClientCore.Internals; using WebApiClientCore.Serialization; namespace WebApiClientCore.HttpContents @@ -50,17 +49,21 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions) [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) { + JsonBufferSerializer.Serialize(this, value, jsonSerializerOptions); + if (encoding == null || Encoding.UTF8.Equals(encoding)) { this.Headers.ContentType = defaultMediaType; - JsonBufferSerializer.Serialize(this, value, jsonSerializerOptions); } else { this.Headers.ContentType = new MediaTypeHeaderValue(mediaType) { CharSet = encoding.WebName }; - using var utf8Writer = new RecyclableBufferWriter(); - JsonBufferSerializer.Serialize(utf8Writer, value, jsonSerializerOptions); - Encoding.UTF8.Convert(encoding, utf8Writer.WrittenSpan, this); + + var utf8Json = this.WrittenSegment; + var encodingBuffer = Encoding.Convert(Encoding.UTF8, encoding, utf8Json.Array!, utf8Json.Offset, utf8Json.Count); + + this.Clear(); + this.Write(encodingBuffer); } } } From f9a5dbe8a92a16c320d0f503d72ef659e27c467c Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 04:53:05 +0800 Subject: [PATCH 089/108] =?UTF-8?q?JsonPatchDocument=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8JsonPatchContent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/Parameters/JsonPatchDocument.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore/Parameters/JsonPatchDocument.cs b/WebApiClientCore/Parameters/JsonPatchDocument.cs index c1896a14..1e34a105 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -3,9 +3,10 @@ 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 { @@ -13,11 +14,10 @@ namespace WebApiClientCore.Parameters /// 表示将自身作为JsonPatch请求内容 /// [DebuggerTypeProxy(typeof(DebugView))] + [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 { - /// - /// 操作列表 - /// + private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new("application/json-patch+json"); private readonly List operations = []; /// @@ -79,7 +79,7 @@ public Task OnRequestAsync(ApiParameterContext context) } var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = new JsonPatchContent(this.operations, options); + context.HttpContext.RequestMessage.Content = JsonContent.Create(this.operations, this.operations.GetType(), mediaTypeHeaderValue, options); return Task.CompletedTask; } From 8cb84575db7ae82a6b7331f81340f22ddf8ff76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 21 Jun 2024 09:47:29 +0800 Subject: [PATCH 090/108] =?UTF-8?q?=E5=A2=9E=E5=8A=A0IChunkedable=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JsonContentAttributeTest.cs | 27 ++++++++++++++-- .../HttpContents/JsonContentTest.cs | 31 +++++++++++++++++++ .../JsonContentAttribute.cs | 20 ++++++++++-- WebApiClientCore/HttpContents/JsonContent.cs | 14 ++++++--- WebApiClientCore/HttpContents/XmlContent.cs | 9 ++++-- WebApiClientCore/IChunkedable.cs | 13 ++++++++ .../Parameters/JsonPatchDocument.cs | 18 +++++++++-- .../Serialization/XmlSerializer.cs | 12 ++++--- 8 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 WebApiClientCore.Test/HttpContents/JsonContentTest.cs create mode 100644 WebApiClientCore/IChunkedable.cs 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/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/Attributes/ParameterAttributes/JsonContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs index d723eed0..ff3284f2 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/JsonContentAttribute.cs @@ -11,7 +11,7 @@ namespace WebApiClientCore.Attributes /// 使用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); @@ -19,6 +19,12 @@ public class JsonContentAttribute : HttpContentAttribute, ICharSetable private MediaTypeHeaderValue mediaType = defaultMediaType; private Encoding encoding = Encoding.UTF8; + /// + /// 获取或设置是否允许 chunked 传输 + /// 默认为 true + /// + public bool AllowChunked { get; set; } = true; + /// /// 获取或设置编码名称 /// @@ -43,9 +49,17 @@ public string CharSet protected override Task SetHttpContentAsync(ApiParameterContext context) { var value = context.ParameterValue; - var valueType = value == null ? context.Parameter.ParameterType : value.GetType(); var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = JsonContent.Create(value, valueType, this.mediaType, options); + + 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/HttpContents/JsonContent.cs b/WebApiClientCore/HttpContents/JsonContent.cs index bc5a4aa6..1ec14e37 100644 --- a/WebApiClientCore/HttpContents/JsonContent.cs +++ b/WebApiClientCore/HttpContents/JsonContent.cs @@ -58,13 +58,17 @@ public JsonContent(object? value, JsonSerializerOptions? jsonSerializerOptions, else { this.Headers.ContentType = new MediaTypeHeaderValue(mediaType) { CharSet = encoding.WebName }; + this.ConvertEncoding(encoding); + } + } - var utf8Json = this.WrittenSegment; - var encodingBuffer = Encoding.Convert(Encoding.UTF8, encoding, utf8Json.Array!, utf8Json.Offset, utf8Json.Count); + 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); - } + this.Clear(); + this.Write(encodingBuffer); } } } diff --git a/WebApiClientCore/HttpContents/XmlContent.cs b/WebApiClientCore/HttpContents/XmlContent.cs index f0a525df..38be80b4 100644 --- a/WebApiClientCore/HttpContents/XmlContent.cs +++ b/WebApiClientCore/HttpContents/XmlContent.cs @@ -26,7 +26,9 @@ public class XmlContent : BufferContent public XmlContent(string? xml, Encoding encoding) { encoding.GetBytes(xml, this); - this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; + this.Headers.ContentType = encoding == Encoding.UTF8 + ? defaultMediaType : + new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; } /// @@ -38,8 +40,9 @@ public XmlContent(string? xml, Encoding encoding) public XmlContent(object? obj, XmlWriterSettings xmlWriterSettings) { XmlSerializer.Serialize(this, obj, xmlWriterSettings); - var encoding = xmlWriterSettings.Encoding; - this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; + this.Headers.ContentType = xmlWriterSettings.Encoding == Encoding.UTF8 + ? defaultMediaType + : new MediaTypeHeaderValue(MediaType) { CharSet = xmlWriterSettings.Encoding.WebName }; } } } \ No newline at end of file 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/Parameters/JsonPatchDocument.cs b/WebApiClientCore/Parameters/JsonPatchDocument.cs index 1e34a105..4c95e927 100644 --- a/WebApiClientCore/Parameters/JsonPatchDocument.cs +++ b/WebApiClientCore/Parameters/JsonPatchDocument.cs @@ -15,11 +15,17 @@ namespace WebApiClientCore.Parameters /// [DebuggerTypeProxy(typeof(DebugView))] [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 + public class JsonPatchDocument : IApiParameter, IChunkedable { private static readonly MediaTypeHeaderValue mediaTypeHeaderValue = new("application/json-patch+json"); private readonly List operations = []; + /// + /// 获取或设置是否允许 chunked 传输 + /// 默认为 true + /// + public bool AllowChunked { get; set; } = true; + /// /// Add操作 /// @@ -79,8 +85,14 @@ public Task OnRequestAsync(ApiParameterContext context) } var options = context.HttpContext.HttpApiOptions.JsonSerializeOptions; - context.HttpContext.RequestMessage.Content = JsonContent.Create(this.operations, this.operations.GetType(), mediaTypeHeaderValue, 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; } diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index 30db5688..8489f75e 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -33,9 +33,7 @@ public static class XmlSerializer var settings = options ?? writerSettings; using var writer = new EncodingStringWriter(settings.Encoding); - using var xmlWriter = XmlWriter.Create(writer, settings); - var xmlSerializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); - xmlSerializer.Serialize(xmlWriter, obj); + SerializeCore(writer, obj, settings); return writer.ToString(); } @@ -55,7 +53,13 @@ public static void Serialize(IBufferWriter bufferWriter, object? obj, XmlW var settings = options ?? writerSettings; using var writer = new BufferTextWriter(bufferWriter, settings.Encoding); - using var xmlWriter = XmlWriter.Create(writer, settings); + SerializeCore(writer, obj, settings); + } + + [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] + private static void SerializeCore(TextWriter textWriter, object obj, XmlWriterSettings settings) + { + using var xmlWriter = XmlWriter.Create(textWriter, settings); var xmlSerializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); xmlSerializer.Serialize(xmlWriter, obj); } From 27ae5bbc5fab2d08f159d534df2558e0a1a62151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 21 Jun 2024 13:08:41 +0800 Subject: [PATCH 091/108] =?UTF-8?q?=E4=BC=98=E5=8C=96xml=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E5=88=B0IBufferWriter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Benchmarks/Program.cs | 2 +- .../XmlContentAttributeTest.cs | 4 +- .../ApiParameterContextExtensions.cs | 17 ++++++- .../XmlContentAttribute.cs | 12 ++--- WebApiClientCore/HttpContents/XmlContent.cs | 20 +------- .../Serialization/XmlSerializer.cs | 50 +++++-------------- 6 files changed, 36 insertions(+), 69 deletions(-) diff --git a/WebApiClientCore.Benchmarks/Program.cs b/WebApiClientCore.Benchmarks/Program.cs index a616982e..4969a42e 100644 --- a/WebApiClientCore.Benchmarks/Program.cs +++ b/WebApiClientCore.Benchmarks/Program.cs @@ -9,8 +9,8 @@ static void Main(string[] args) { BenchmarkRunner.Run(); BenchmarkRunner.Run(); - BenchmarkRunner.Run(); BenchmarkRunner.Run(); + BenchmarkRunner.Run(); BenchmarkRunner.Run(); } } diff --git a/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs b/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs index 6e7d5d4b..4cab65e6 100644 --- a/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs +++ b/WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs @@ -37,9 +37,7 @@ public async Task OnRequestAsyncTest() await attr.OnRequestAsync(new ApiParameterContext(context, 0)); var body = await context.HttpContext.RequestMessage.Content!.ReadAsStringAsync(); - using var bufferWriter = new RecyclableBufferWriter(); - XmlSerializer.Serialize(bufferWriter, context.Arguments[0], null); - var target = Encoding.GetEncoding(attr.CharSet).GetString(bufferWriter.WrittenSpan); + var target = XmlSerializer.Serialize(context.Arguments[0], null); Assert.True(body == target); } } diff --git a/WebApiClientCore/ApiParameterContextExtensions.cs b/WebApiClientCore/ApiParameterContextExtensions.cs index 2bd46e77..aba8b119 100644 --- a/WebApiClientCore/ApiParameterContextExtensions.cs +++ b/WebApiClientCore/ApiParameterContextExtensions.cs @@ -89,6 +89,21 @@ public static void SerializeToJson(this ApiParameterContext context, IBufferWrit /// [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) @@ -97,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); } /// diff --git a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs index 37d85a8b..fbc0bd2e 100644 --- a/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs +++ b/WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs @@ -3,6 +3,7 @@ using System.Text; using System.Threading.Tasks; using WebApiClientCore.HttpContents; +using WebApiClientCore.Internals; namespace WebApiClientCore.Attributes { @@ -34,14 +35,9 @@ public string CharSet [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 options = context.HttpContext.HttpApiOptions.XmlSerializeOptions; - if (encoding != null && encoding.Equals(options.Encoding) == false) - { - options = options.Clone(); - options.Encoding = encoding; - } - - context.HttpContext.RequestMessage.Content = new XmlContent(context.ParameterValue, options); + 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/HttpContents/XmlContent.cs b/WebApiClientCore/HttpContents/XmlContent.cs index 38be80b4..665b0d97 100644 --- a/WebApiClientCore/HttpContents/XmlContent.cs +++ b/WebApiClientCore/HttpContents/XmlContent.cs @@ -1,8 +1,6 @@ -using System.Diagnostics.CodeAnalysis; +using System; using System.Net.Http.Headers; using System.Text; -using System.Xml; -using WebApiClientCore.Serialization; namespace WebApiClientCore.HttpContents { @@ -23,26 +21,12 @@ public class XmlContent : BufferContent /// /// xml内容 /// 编码 - public XmlContent(string? xml, Encoding encoding) + public XmlContent(ReadOnlySpan xml, Encoding encoding) { encoding.GetBytes(xml, this); this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName }; } - - /// - /// xml内容 - /// - /// xml实体 - /// xml写入设置项 - [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] - public XmlContent(object? obj, XmlWriterSettings xmlWriterSettings) - { - XmlSerializer.Serialize(this, obj, xmlWriterSettings); - this.Headers.ContentType = xmlWriterSettings.Encoding == Encoding.UTF8 - ? defaultMediaType - : new MediaTypeHeaderValue(MediaType) { CharSet = xmlWriterSettings.Encoding.WebName }; - } } } \ No newline at end of file diff --git a/WebApiClientCore/Serialization/XmlSerializer.cs b/WebApiClientCore/Serialization/XmlSerializer.cs index 8489f75e..f4d68b9d 100644 --- a/WebApiClientCore/Serialization/XmlSerializer.cs +++ b/WebApiClientCore/Serialization/XmlSerializer.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml; +using WebApiClientCore.Internals; namespace WebApiClientCore.Serialization { @@ -31,10 +32,9 @@ public static class XmlSerializer return null; } - var settings = options ?? writerSettings; - using var writer = new EncodingStringWriter(settings.Encoding); - SerializeCore(writer, obj, settings); - return writer.ToString(); + using var bufferWriter = new RecyclableBufferWriter(); + Serialize(obj, options, bufferWriter); + return bufferWriter.WrittenSpan.ToString(); } /// @@ -44,7 +44,7 @@ public static class XmlSerializer /// 对象 /// 配置选项 [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] - public static void Serialize(IBufferWriter bufferWriter, object? obj, XmlWriterSettings? options) + public static void Serialize(object? obj, XmlWriterSettings? options, IBufferWriter bufferWriter) { if (obj == null) { @@ -52,14 +52,8 @@ public static void Serialize(IBufferWriter bufferWriter, object? obj, XmlW } var settings = options ?? writerSettings; - using var writer = new BufferTextWriter(bufferWriter, settings.Encoding); - SerializeCore(writer, obj, settings); - } - - [RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")] - private static void SerializeCore(TextWriter textWriter, object obj, XmlWriterSettings settings) - { - using var xmlWriter = XmlWriter.Create(textWriter, settings); + 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); } @@ -92,17 +86,18 @@ private static void SerializeCore(TextWriter textWriter, object obj, XmlWriterSe } - private class BufferTextWriter : TextWriter + private class XmlBufferWriter : TextWriter { - private readonly IBufferWriter bufferWriter; + private readonly IBufferWriter bufferWriter; public override Encoding Encoding { get; } - public BufferTextWriter(IBufferWriter bufferWriter, Encoding encoding) + public XmlBufferWriter(IBufferWriter bufferWriter, Encoding encoding) { this.bufferWriter = bufferWriter; this.Encoding = encoding; } + public override Task FlushAsync() { return Task.CompletedTask; @@ -110,7 +105,7 @@ public override Task FlushAsync() public override void Write(ReadOnlySpan buffer) { - this.Encoding.GetBytes(buffer, this.bufferWriter); + this.bufferWriter.Write(buffer); } public override void Write(char value) @@ -187,26 +182,5 @@ public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationTok return Task.CompletedTask; } } - - - /// - /// 表示可指定编码文本写入器 - /// - private class EncodingStringWriter : StringWriter - { - /// - /// 获取编码 - /// - public override Encoding Encoding { get; } - - /// - /// 可指定编码文本写入器 - /// - /// 编码 - public EncodingStringWriter(Encoding encoding) - { - this.Encoding = encoding; - } - } } } From 0e188406b129a89d501100c8070892c1841965d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 21 Jun 2024 13:32:54 +0800 Subject: [PATCH 092/108] =?UTF-8?q?=E5=87=8F=E5=B0=91=20AddForm(string=3F?= =?UTF-8?q?=20encodedForm)=E7=9A=84=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore/HttpContents/FormContent.cs | 29 ++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/WebApiClientCore/HttpContents/FormContent.cs b/WebApiClientCore/HttpContents/FormContent.cs index 3dc19c6a..9d80a130 100644 --- a/WebApiClientCore/HttpContents/FormContent.cs +++ b/WebApiClientCore/HttpContents/FormContent.cs @@ -111,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); } /// @@ -130,16 +133,14 @@ public void AddForm(ReadOnlySpan encodedForm) { this.EnsureNotBuffered(); - if (encodedForm.IsEmpty == true) + if (encodedForm.Length > 0) { - return; - } - - if (this.bufferWriter.WrittenCount > 0) - { - this.bufferWriter.Write((byte)'&'); + if (this.bufferWriter.WrittenCount > 0) + { + this.bufferWriter.Write((byte)'&'); + } + this.bufferWriter.Write(encodedForm); } - this.bufferWriter.Write(encodedForm); } /// From 51a40a6515b573296527f24504a9ff25ece36055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 21 Jun 2024 13:48:44 +0800 Subject: [PATCH 093/108] update benchmark results --- ...arks.Requests.HttpGetBenchmark-report.html | 30 +++++++++++++++++++ ....Requests.HttpGetJsonBenchmark-report.html | 30 +++++++++++++++++++ ...Requests.HttpPostJsonBenchmark-report.html | 30 +++++++++++++++++++ ....Requests.HttpPostXmlBenchmark-report.html | 30 +++++++++++++++++++ ....Requests.HttpPutFormBenchmark-report.html | 30 +++++++++++++++++++ 5 files changed, 150 insertions(+) create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html new file mode 100644 index 00000000..33fe52cd --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html @@ -0,0 +1,30 @@ + + + + +WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-20240621-134144 + + + + +

+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 MeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_GetAsync5.926 μs0.1162 μs0.1630 μs1.000.000.30523.45 KB1.00
Refit_GetAsync15.943 μs0.2997 μs0.3681 μs2.700.100.48835.18 KB1.50
+ + diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html new file mode 100644 index 00000000..d9f84ebd --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html @@ -0,0 +1,30 @@ + + + + +WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-20240621-134158 + + + + +

+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 MeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_GetJsonAsync11.64 μs0.232 μs0.495 μs11.44 μs1.000.000.41204.3 KB1.00
Refit_GetJsonAsync25.36 μs0.496 μs0.830 μs25.03 μs2.190.130.54935.67 KB1.32
+ + diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html new file mode 100644 index 00000000..ee4f99b1 --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html @@ -0,0 +1,30 @@ + + + + +WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-20240621-134335 + + + + +

+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 MeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_PostJsonAsync11.24 μs0.223 μs0.441 μs1.000.000.41204.23 KB1.00
Refit_PostJsonAsync24.80 μs0.487 μs0.598 μs2.180.090.57986.08 KB1.44
+ + diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html new file mode 100644 index 00000000..1a0a5930 --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html @@ -0,0 +1,30 @@ + + + + +WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-20240621-134451 + + + + +

+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 MeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
WebApiClientCore_PostXmlAsync48.34 μs0.926 μs0.866 μs1.000.003.4180-35.48 KB1.00
Refit_PostXmlAsync59.37 μs1.180 μs2.961 μs1.200.0614.03812.3193144.38 KB4.07
+ + diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html new file mode 100644 index 00000000..89334b5f --- /dev/null +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html @@ -0,0 +1,30 @@ + + + + +WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-20240621-134621 + + + + +

+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 MeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_PutFormAsync20.10 μs0.395 μs0.470 μs19.96 μs1.000.000.54935.7 KB1.00
Refit_PutFormAsync75.53 μs1.476 μs2.163 μs74.24 μs3.780.121.098611.57 KB2.03
+ + From f3a489541e16d019992e488ea04393f34fa857f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 21 Jun 2024 13:50:07 +0800 Subject: [PATCH 094/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=BAmd=E6=A0=BC?= =?UTF-8?q?=E5=BC=8Fresult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Requests.HttpGetBenchmark-report-github.md | 13 ++++++++ ...arks.Requests.HttpGetBenchmark-report.html | 30 ------------------- ...ests.HttpGetJsonBenchmark-report-github.md | 13 ++++++++ ....Requests.HttpGetJsonBenchmark-report.html | 30 ------------------- ...sts.HttpPostJsonBenchmark-report-github.md | 13 ++++++++ ...Requests.HttpPostJsonBenchmark-report.html | 30 ------------------- ...ests.HttpPostXmlBenchmark-report-github.md | 13 ++++++++ ....Requests.HttpPostXmlBenchmark-report.html | 30 ------------------- ...ests.HttpPutFormBenchmark-report-github.md | 13 ++++++++ ....Requests.HttpPutFormBenchmark-report.html | 30 ------------------- 10 files changed, 65 insertions(+), 150 deletions(-) create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html create mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report-github.md delete mode 100644 WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html 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..fccbcef1 --- /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.926 μs | 0.1162 μs | 0.1630 μs | 1.00 | 0.00 | 0.3052 | 3.45 KB | 1.00 | +| Refit_GetAsync | 15.943 μs | 0.2997 μs | 0.3681 μs | 2.70 | 0.10 | 0.4883 | 5.18 KB | 1.50 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html deleted file mode 100644 index 33fe52cd..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - -WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-20240621-134144 - - - - -

-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 MeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_GetAsync5.926 μs0.1162 μs0.1630 μs1.000.000.30523.45 KB1.00
Refit_GetAsync15.943 μs0.2997 μs0.3681 μs2.700.100.48835.18 KB1.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..64d8b2f6 --- /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 | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------ |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_GetJsonAsync | 11.64 μs | 0.232 μs | 0.495 μs | 11.44 μs | 1.00 | 0.00 | 0.4120 | 4.3 KB | 1.00 | +| Refit_GetJsonAsync | 25.36 μs | 0.496 μs | 0.830 μs | 25.03 μs | 2.19 | 0.13 | 0.5493 | 5.67 KB | 1.32 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html deleted file mode 100644 index d9f84ebd..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - -WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-20240621-134158 - - - - -

-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 MeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_GetJsonAsync11.64 μs0.232 μs0.495 μs11.44 μs1.000.000.41204.3 KB1.00
Refit_GetJsonAsync25.36 μs0.496 μs0.830 μs25.03 μs2.190.130.54935.67 KB1.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..57ef05c1 --- /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.24 μs | 0.223 μs | 0.441 μs | 1.00 | 0.00 | 0.4120 | 4.23 KB | 1.00 | +| Refit_PostJsonAsync | 24.80 μs | 0.487 μs | 0.598 μs | 2.18 | 0.09 | 0.5798 | 6.08 KB | 1.44 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html deleted file mode 100644 index ee4f99b1..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - -WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-20240621-134335 - - - - -

-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 MeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_PostJsonAsync11.24 μs0.223 μs0.441 μs1.000.000.41204.23 KB1.00
Refit_PostJsonAsync24.80 μs0.487 μs0.598 μs2.180.090.57986.08 KB1.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..4ed1e727 --- /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 | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------------------------------ |---------:|---------:|---------:|------:|--------:|--------:|-------:|----------:|------------:| +| WebApiClientCore_PostXmlAsync | 48.34 μs | 0.926 μs | 0.866 μs | 1.00 | 0.00 | 3.4180 | - | 35.48 KB | 1.00 | +| Refit_PostXmlAsync | 59.37 μs | 1.180 μs | 2.961 μs | 1.20 | 0.06 | 14.0381 | 2.3193 | 144.38 KB | 4.07 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html deleted file mode 100644 index 1a0a5930..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - -WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-20240621-134451 - - - - -

-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 MeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
WebApiClientCore_PostXmlAsync48.34 μs0.926 μs0.866 μs1.000.003.4180-35.48 KB1.00
Refit_PostXmlAsync59.37 μs1.180 μs2.961 μs1.200.0614.03812.3193144.38 KB4.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..1b724673 --- /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 | 20.10 μs | 0.395 μs | 0.470 μs | 19.96 μs | 1.00 | 0.00 | 0.5493 | 5.7 KB | 1.00 | +| Refit_PutFormAsync | 75.53 μs | 1.476 μs | 2.163 μs | 74.24 μs | 3.78 | 0.12 | 1.0986 | 11.57 KB | 2.03 | diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html deleted file mode 100644 index 89334b5f..00000000 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - -WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-20240621-134621 - - - - -

-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 MeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc Ratio
WebApiClientCore_PutFormAsync20.10 μs0.395 μs0.470 μs19.96 μs1.000.000.54935.7 KB1.00
Refit_PutFormAsync75.53 μs1.476 μs2.163 μs74.24 μs3.780.121.098611.57 KB2.03
- - From 05c68f3d1bef8111f914634baaec36f4fb9dce5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 21 Jun 2024 14:00:19 +0800 Subject: [PATCH 095/108] =?UTF-8?q?xml=E6=9C=8D=E5=8A=A1=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=A2=84=E7=83=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApiClientCore.Benchmarks/Requests/Benchmark.cs | 4 ++++ ....Benchmarks.Requests.HttpGetBenchmark-report-github.md | 4 ++-- ...chmarks.Requests.HttpGetJsonBenchmark-report-github.md | 8 ++++---- ...hmarks.Requests.HttpPostJsonBenchmark-report-github.md | 4 ++-- ...chmarks.Requests.HttpPostXmlBenchmark-report-github.md | 8 ++++---- ...chmarks.Requests.HttpPutFormBenchmark-report-github.md | 4 ++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs index 2a2adec8..8c6bbb82 100644 --- a/WebApiClientCore.Benchmarks/Requests/Benchmark.cs +++ b/WebApiClientCore.Benchmarks/Requests/Benchmark.cs @@ -58,8 +58,12 @@ public void GlobalSetup() .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://webapiclient.com/")); this.ServiceProvider = services.BuildServiceProvider(); + + // 服务显式加载预热 this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); this.ServiceProvider.GetService(); + this.ServiceProvider.GetService(); } private class JsonResponseHandler : DelegatingHandler diff --git a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md index fccbcef1..efe7b7ff 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetBenchmark-report-github.md @@ -9,5 +9,5 @@ Job=InProcess Toolchain=InProcessEmitToolchain ``` | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |-------------------------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_GetAsync | 5.926 μs | 0.1162 μs | 0.1630 μs | 1.00 | 0.00 | 0.3052 | 3.45 KB | 1.00 | -| Refit_GetAsync | 15.943 μs | 0.2997 μs | 0.3681 μs | 2.70 | 0.10 | 0.4883 | 5.18 KB | 1.50 | +| 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 index 64d8b2f6..ba3476ec 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpGetJsonBenchmark-report-github.md @@ -7,7 +7,7 @@ Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores Job=InProcess Toolchain=InProcessEmitToolchain ``` -| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|------------------------------ |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_GetJsonAsync | 11.64 μs | 0.232 μs | 0.495 μs | 11.44 μs | 1.00 | 0.00 | 0.4120 | 4.3 KB | 1.00 | -| Refit_GetJsonAsync | 25.36 μs | 0.496 μs | 0.830 μs | 25.03 μs | 2.19 | 0.13 | 0.5493 | 5.67 KB | 1.32 | +| 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 index 57ef05c1..cac90723 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostJsonBenchmark-report-github.md @@ -9,5 +9,5 @@ Job=InProcess Toolchain=InProcessEmitToolchain ``` | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |------------------------------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_PostJsonAsync | 11.24 μs | 0.223 μs | 0.441 μs | 1.00 | 0.00 | 0.4120 | 4.23 KB | 1.00 | -| Refit_PostJsonAsync | 24.80 μs | 0.487 μs | 0.598 μs | 2.18 | 0.09 | 0.5798 | 6.08 KB | 1.44 | +| 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 index 4ed1e727..5f090cda 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPostXmlBenchmark-report-github.md @@ -7,7 +7,7 @@ Intel Xeon CPU E5-2650 v2 2.60GHz, 2 CPU, 32 logical and 16 physical cores Job=InProcess Toolchain=InProcessEmitToolchain ``` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|------------------------------ |---------:|---------:|---------:|------:|--------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_PostXmlAsync | 48.34 μs | 0.926 μs | 0.866 μs | 1.00 | 0.00 | 3.4180 | - | 35.48 KB | 1.00 | -| Refit_PostXmlAsync | 59.37 μs | 1.180 μs | 2.961 μs | 1.20 | 0.06 | 14.0381 | 2.3193 | 144.38 KB | 4.07 | +| 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 index 1b724673..7e1f4f5f 100644 --- a/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report-github.md +++ b/WebApiClientCore.Benchmarks/results/WebApiClientCore.Benchmarks.Requests.HttpPutFormBenchmark-report-github.md @@ -9,5 +9,5 @@ Job=InProcess Toolchain=InProcessEmitToolchain ``` | Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |------------------------------ |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| -| WebApiClientCore_PutFormAsync | 20.10 μs | 0.395 μs | 0.470 μs | 19.96 μs | 1.00 | 0.00 | 0.5493 | 5.7 KB | 1.00 | -| Refit_PutFormAsync | 75.53 μs | 1.476 μs | 2.163 μs | 74.24 μs | 3.78 | 0.12 | 1.0986 | 11.57 KB | 2.03 | +| 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 | From 0b3513a7497b52c182ce116bcecf700701cf1d8c Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 21:04:40 +0800 Subject: [PATCH 096/108] =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ApiRequestExecuter.cs => ApiRequestExecutor.cs} | 2 +- WebApiClientCore/Implementations/DefaultApiActionInvoker.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename WebApiClientCore/Implementations/{ApiRequestExecuter.cs => ApiRequestExecutor.cs} (99%) diff --git a/WebApiClientCore/Implementations/ApiRequestExecuter.cs b/WebApiClientCore/Implementations/ApiRequestExecutor.cs similarity index 99% rename from WebApiClientCore/Implementations/ApiRequestExecuter.cs rename to WebApiClientCore/Implementations/ApiRequestExecutor.cs index 0ba82447..792838ca 100644 --- a/WebApiClientCore/Implementations/ApiRequestExecuter.cs +++ b/WebApiClientCore/Implementations/ApiRequestExecutor.cs @@ -9,7 +9,7 @@ namespace WebApiClientCore.Implementations /// /// 请求上下文执行器 /// - static class ApiRequestExecuter + static class ApiRequestExecutor { /// /// 执行上下文 diff --git a/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs b/WebApiClientCore/Implementations/DefaultApiActionInvoker.cs index febce382..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; From 7f451f3a489f2ddf05281cb35fb3d964d3bf482a Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Fri, 21 Jun 2024 23:18:31 +0800 Subject: [PATCH 097/108] =?UTF-8?q?=E5=A3=B0=E6=98=8E=E5=AF=B9HttpContent?= =?UTF-8?q?=E7=9A=84NonPublicProperties=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../System.Net.Http/HttpContentExtensions.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs index e1c7050e..b9a19da6 100644 --- a/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs +++ b/WebApiClientCore/System.Net.Http/HttpContentExtensions.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; @@ -24,7 +25,8 @@ public static class HttpContentExtensions /// /// 静态构造器 - /// + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, typeof(HttpContent))] static HttpContentExtensions() { var property = typeof(HttpContent).GetProperty(IsBufferedPropertyName, BindingFlags.Instance | BindingFlags.NonPublic); @@ -70,8 +72,8 @@ public static void EnsureNotBuffered(this HttpContent httpContent) { throw new HttpContentBufferedException(); } - } - + } + /// /// 读取为二进制数组并转换为 utf8 编码 /// @@ -133,7 +135,7 @@ public static Encoding GetEncoding(this HttpContent httpContent) return encoding.Equals(Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) ? Encoding.UTF8 : Encoding.GetEncoding(encoding.ToString()); - } + } } } From 3e0db62981e6449d2fd191b46ae2dc86e41ff80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Mon, 24 Jun 2024 09:06:06 +0800 Subject: [PATCH 098/108] =?UTF-8?q?AddFormDataText=E7=AE=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpApiRequestMessageImpl.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs index 20e30800..8de2e9cf 100644 --- a/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs +++ b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs @@ -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的请求消息 @@ -123,12 +123,7 @@ private static string GetRelativeUri(Uri uri) var path = uri.OriginalString.AsSpan()[(uri.Scheme.Length + 3)..]; var index = path.IndexOf('/'); - if (index < 0) - { - return "/"; - } - - return path[index..].ToString(); + return index < 0 ? "/" : path[index..].ToString(); } /// @@ -182,16 +177,16 @@ 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(); + this.Content = httpContent; } foreach (var keyValue in keyValues) { var textContent = new FormDataTextContent(keyValue); httpContent.Add(textContent); - this.Content = httpContent; } } @@ -209,14 +204,14 @@ 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; } /// From 91fceee6cc9c3df865519dbea1796a0d1ae1a9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Sat, 29 Jun 2024 14:03:42 +0800 Subject: [PATCH 099/108] =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=90=8D=E5=8A=A0@?= =?UTF-8?q?=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 30874138..9b99e298 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -112,8 +112,8 @@ public override string ToString() 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 parametersString = string.Join(", ", method.Parameters.Select(item => $"{GetFullName(item.Type)} @{item.Name}")); + var parameterNamesString = string.Join(", ", method.Parameters.Select(item => "@" + item.Name)); var parameterArrayString = string.IsNullOrEmpty(parameterNamesString) ? "global::System.Array.Empty()" : $"new global::System.Object[] {{ {parameterNamesString} }}"; From 01a36b3a3b1e9d3110b0ada348beff6801a800f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Sat, 29 Jun 2024 14:43:12 +0800 Subject: [PATCH 100/108] =?UTF-8?q?=E4=BD=BF=E7=94=A8p=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceGenerator/HttpApiProxyClass.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs index 9b99e298..d704f0b1 100644 --- a/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs +++ b/WebApiClientCore.Analyzers/SourceGenerator/HttpApiProxyClass.cs @@ -112,8 +112,8 @@ public override string ToString() 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 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} }}"; From b6fd8242b99dd9d5b7abfc24862c63fc556abbd5 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Sun, 30 Jun 2024 20:23:33 +0800 Subject: [PATCH 101/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- WebApiClientCore/WebApiClientCore.csproj | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index cb1c6abf..8277043f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.1.3 + 2.1.4 Copyright © laojiu 2017-2024 IDE0290;NETSDK1138 diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 11cc7796..04dc34f1 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -11,16 +11,25 @@ true Sign.snk - + - + + - - + + + + + + + + + + From 88bcba1cc43da2c32fc2c8c88461e6a9ed13c293 Mon Sep 17 00:00:00 2001 From: xljiulang <366193849@qq.com> Date: Tue, 23 Jul 2024 22:31:29 +0800 Subject: [PATCH 102/108] GetRequiredService --- .../Attributes/JsonNetContentAttribute.cs | 2 +- .../Attributes/JsonNetReturnAttribute.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs index cb68e50c..b9abff65 100644 --- a/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs +++ b/WebApiClientCore.Extensions.NewtonsoftJson/Attributes/JsonNetContentAttribute.cs @@ -39,7 +39,7 @@ public string CharSet 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); } From 50459cb1227e37f6de0b238ba5b6969e9943abaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Fri, 27 Sep 2024 14:07:33 +0800 Subject: [PATCH 103/108] =?UTF-8?q?xml=E5=85=83=E7=B4=A0=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E8=BD=AC=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/HttpApi.cshtml | 16 +++++++++------- .../Views/HttpModel.cshtml | 3 ++- ...bApiClientCore.OpenApi.SourceGenerator.csproj | 14 +++++--------- 3 files changed, 16 insertions(+), 17 deletions(-) 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..b8f9053a 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj +++ b/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj @@ -3,6 +3,7 @@ Exe enable + 2.1.4.1 netcoreapp3.1;net6.0;net8.0 将本地或远程OpenApi文档解析生成WebApiClientCore的接口定义代码文件的工具 @@ -24,16 +25,11 @@ PreserveNewest - + PreserveNewest $(IncludeRazorContentInPack) - PreserveNewest - - - PreserveNewest - $(IncludeRazorContentInPack) - PreserveNewest - - + Always + + From a531b3b83c9313ca1b6ae1a47900e6a3c53091e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:53:57 +0000 Subject: [PATCH 104/108] Bump Microsoft.Extensions.Caching.Memory in /WebApiClientCore Bumps [Microsoft.Extensions.Caching.Memory](https://github.com/dotnet/runtime) from 8.0.0 to 8.0.1. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v8.0.0...v8.0.1) --- updated-dependencies: - dependency-name: Microsoft.Extensions.Caching.Memory dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- WebApiClientCore/WebApiClientCore.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 04dc34f1..2d6d4ebe 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -15,20 +15,20 @@ - + - + - + - + - + From 387f478979f1c5d61327946890c04373fa4b8c53 Mon Sep 17 00:00:00 2001 From: Ezreal Date: Sat, 19 Oct 2024 09:17:01 +0800 Subject: [PATCH 105/108] Revert "Bump Microsoft.Extensions.Caching.Memory in /WebApiClientCore" This reverts commit a531b3b83c9313ca1b6ae1a47900e6a3c53091e0. --- WebApiClientCore/WebApiClientCore.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 2d6d4ebe..04dc34f1 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -15,20 +15,20 @@ - + - + - + - + - + From 2725ef8e293379ea0a5c9b88a2119b6d349c67d8 Mon Sep 17 00:00:00 2001 From: Ezreal Date: Sat, 19 Oct 2024 09:19:44 +0800 Subject: [PATCH 106/108] Security Update CVE-2024-43483 --- WebApiClientCore/WebApiClientCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebApiClientCore/WebApiClientCore.csproj b/WebApiClientCore/WebApiClientCore.csproj index 04dc34f1..8e6d8bd3 100644 --- a/WebApiClientCore/WebApiClientCore.csproj +++ b/WebApiClientCore/WebApiClientCore.csproj @@ -28,7 +28,7 @@ - + From 78fbaf79e58e0f6c83766261422795837700361b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 28 Nov 2024 15:08:52 +0800 Subject: [PATCH 107/108] =?UTF-8?q?=E6=8F=90=E5=8D=87=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- ...WebApiClientCore.OpenApi.SourceGenerator.csproj | 1 - WebApiClientCore/Parameters/FormDataFile.cs | 14 ++------------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8277043f..4b473b3a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.1.4 + 2.1.5 Copyright © laojiu 2017-2024 IDE0290;NETSDK1138 diff --git a/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj b/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj index b8f9053a..e4f264f2 100644 --- a/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj +++ b/WebApiClientCore.OpenApi.SourceGenerator/WebApiClientCore.OpenApi.SourceGenerator.csproj @@ -3,7 +3,6 @@ Exe enable - 2.1.4.1 netcoreapp3.1;net6.0;net8.0 将本地或远程OpenApi文档解析生成WebApiClientCore的接口定义代码文件的工具 diff --git a/WebApiClientCore/Parameters/FormDataFile.cs b/WebApiClientCore/Parameters/FormDataFile.cs index bb614011..33624bf5 100644 --- a/WebApiClientCore/Parameters/FormDataFile.cs +++ b/WebApiClientCore/Parameters/FormDataFile.cs @@ -1,12 +1,9 @@ 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 { @@ -22,7 +19,7 @@ public class FormDataFile : IApiParameter private readonly Func streamFactory; /// - /// 获取文件好友名称 + /// 获取文件名称 /// public string? FileName { get; } @@ -30,14 +27,7 @@ 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); - + /// /// multipart/form-data的一个文件项 /// From cfafcbef93aef4e3b2adc96893cc5737dd96e7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=9B=BD=E4=BC=9F?= <366193849@qq.com> Date: Thu, 28 Nov 2024 15:37:56 +0800 Subject: [PATCH 108/108] fix #270 --- .../HttpApiRequestMessageTest.cs | 20 +++++++++++++++---- .../HttpApiRequestMessageImpl.cs | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs b/WebApiClientCore.Test/Implementations/HttpApiRequestMessageTest.cs index c321f93c..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,9 +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}""{"\r\n\r\n"}{HttpUtil.UrlEncode(value, Encoding.UTF8)}"; + return $@"Content-Disposition: form-data; name=""{name}""{"\r\n\r\n"}{value}"; } var reqeust = new HttpApiRequestMessageImpl(); @@ -130,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/Implementations/HttpApiRequestMessageImpl.cs b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs index 8de2e9cf..4789048c 100644 --- a/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs +++ b/WebApiClientCore/Implementations/HttpApiRequestMessageImpl.cs @@ -180,13 +180,13 @@ public override void AddFormDataText(IEnumerable keyValues) if (this.Content is not MultipartContent httpContent) { httpContent = new FormDataContent(); - this.Content = httpContent; } foreach (var keyValue in keyValues) { var textContent = new FormDataTextContent(keyValue); httpContent.Add(textContent); + this.Content = httpContent; } }