From 3c73b930513ed9f28ed0d1672e14018802f9a8ac Mon Sep 17 00:00:00 2001 From: Diego <2248356998@qq.com> Date: Wed, 14 May 2025 18:52:19 +0800 Subject: [PATCH] =?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 --- .../AppHostApplicationBuilderExtensions.cs | 81 +++++ .../App/Extensions/ObjectExtensions.cs | 7 +- .../App/Internal/InternalApp.cs | 28 ++ .../Encryptions/AESEncryption.cs | 160 ++++----- .../Encryptions/GzipEncryption.cs | 92 +++++ .../Extensions/StringEncryptionExtensions.cs | 60 +++- ...ApiControllerApplicationModelConvention.cs | 8 +- .../DynamicApiRuntimeChangeProvider.cs | 2 - .../Builders/EventBusOptionsBuilder.cs | 2 +- .../EventBus/Contexts/EventHandlerContext.cs | 28 ++ .../Contexts/EventHandlerExecutingContext.cs | 14 + .../EventBus/Events/EventHandlerEventArgs.cs | 6 + .../HostedServices/EventBusHostedService.cs | 5 +- .../ThingsGateway.Furion/JWT/JWTEncryption.cs | 9 +- .../Console/ConsoleFormatterExtend.cs | 3 +- .../Console/ConsoleFormatterExtendOptions.cs | 8 + .../Implantations/Database/DatabaseLogger.cs | 2 +- .../Database/DatabaseLoggerOptions.cs | 8 + .../Logging/Implantations/File/FileLogger.cs | 2 +- .../Implantations/File/FileLoggerOptions.cs | 8 + .../Logging/Implantations/LogMessage.cs | 4 +- .../Monitors/LoggingMonitorAttribute.cs | 4 +- .../Monitors/LoggingMonitorSettings.cs | 8 + .../Logging/Internal/Penetrates.cs | 6 +- .../Factories/ISchedulerFactory.Internal.cs | 10 +- .../Factories/SchedulerFactory.Internal.cs | 37 +- .../HostedServices/ScheduleHostedService.cs | 2 +- .../Builders/SpecificationDocumentBuilder.cs | 6 +- .../TimeCrontab/Crontab.Internal.cs | 26 +- .../TimeCrontab/Parsers/RandomParser.cs | 2 +- .../UnifyResult/UnifyContext.cs | 10 +- ...meConverterUsingDateTimeParseAsFallback.cs | 38 ++ ...erterUsingDateTimeOffsetParseAsFallback.cs | 38 ++ .../Converters/Json/StringJsonConverter.cs | 37 ++ .../Extensions/LinqExpressionExtensions.cs | 47 +-- .../Core/Extensions/StringExtensions.cs | 15 +- .../Extensions/Utf8JsonReaderExtensions.cs | 15 + .../Core/Extensions/V5_ObjectExtensions.cs | 39 ++- .../Builders/HttpContextForwardBuilder.cs | 17 +- .../Builders/HttpMultipartFormDataBuilder.cs | 46 ++- .../Builders/HttpRequestBuilder.Methods.cs | 312 ++++++++++++----- .../Builders/HttpRequestBuilder.Properties.cs | 26 +- .../HttpRequestBuilder.StaticMethods.cs | 115 ++++++ .../HttpRemote/Builders/HttpRequestBuilder.cs | 82 ++++- .../HttpRemote/Constants/Constants.cs | 15 +- .../Converters/ObjectContentConverter.cs | 53 ++- .../Converters/VoidContentConverter.cs | 4 +- .../Attributes/HttpVersionAttribute.cs | 30 ++ .../Attributes/RefererAttribute.cs | 30 ++ .../Attributes/SuppressExceptionsAttribute.cs | 45 +++ .../Builders/HttpDeclarativeBuilder.cs | 3 + .../Extractors/HeaderDeclarativeExtractor.cs | 6 +- .../HttpVersionDeclarativeExtractor.cs | 31 ++ .../Extractors/QueryDeclarativeExtractor.cs | 7 +- .../Extractors/RefererDeclarativeExtractor.cs | 31 ++ .../SuppressExceptionsDeclarativeExtractor.cs | 31 ++ .../HttpDeclarativeExtractorContext.cs | 2 +- .../Extensions/HttpContextExtensions.cs | 50 +-- .../HttpMultipartFormDataBuilderExtensions.cs | 88 +++++ .../Extensions/HttpRemoteExtensions.cs | 123 ++++++- .../Factories/HttpContentConverterFactory.cs | 45 ++- .../Factories/IHttpContentConverterFactory.cs | 8 +- .../Managers/FileDownloadManager.cs | 23 +- .../HttpRemote/Managers/FileUploadManager.cs | 8 +- .../HttpRemote/Managers/LongPollingManager.cs | 18 + .../Managers/ServerSentEventsManager.cs | 27 +- .../Managers/StressTestHarnessManager.cs | 9 + .../HttpRemote/Models/HttpRemoteAnalyzer.cs | 4 +- .../HttpRemote/Models/HttpRemoteClient.cs | 186 ++++++++++ .../HttpRemote/Models/HttpRemoteResult.cs | 84 +++++ .../HttpRemote/Models/ServerSentEventsData.cs | 26 +- .../HttpRemote/Options/HttpClientOptions.cs | 34 ++ .../HttpRemote/Options/HttpRemoteOptions.cs | 16 +- .../Processors/StringContentProcessor.cs | 3 +- .../Services/HttpRemoteService.Extensions.cs | 8 +- .../Services/HttpRemoteService.HttpMethods.cs | 130 +++---- .../HttpRemote/Services/HttpRemoteService.cs | 126 +++++-- .../Services/IHttpRemoteService.Extensions.cs | 8 +- .../IHttpRemoteService.HttpMethods.cs | 130 +++---- .../HttpRemote/Services/IHttpRemoteService.cs | 16 +- .../HttpRemote/Utilities/HttpRemoteUtility.cs | 20 +- .../WebSocket/WebSocketBinaryReceiveResult.cs | 2 +- .../WebSocket/WebSocketTextReceiveResult.cs | 2 +- .../RescuePolicy/Policies/FallbackPolicy.cs | 1 + .../RescuePolicy/Policies/RetryPolicy.cs | 1 + .../Shapeless/Attributes/ClayAttribute.cs | 19 + .../Shapeless/Binders/ClayBinder.cs | 30 +- .../Shapeless/Binders/ClayBinderProvider.cs | 14 +- .../Shapeless/Clay/Clay.Enumerable.cs | 16 + .../Shapeless/Clay/Clay.Exports.cs | 327 +++++++++++++++++- .../Shapeless/Clay/Clay.Operator.cs | 147 ++++++++ .../Shapeless/Clay/Clay.Override.cs | 4 + .../V5_Experience/Shapeless/Clay/Clay.cs | 59 +++- .../Converters/ObjectToClayJsonConverter.cs | 40 +++ .../Extensions/ShapelessExtensions.cs | 77 +++++ .../ShapelessMvcBuilderExtensions.cs | 11 + .../Shapeless/Models/ClayEventArgs.cs | 2 +- .../Shapeless/Options/ClayOptions.cs | 28 +- .../ThingsGateway.Razor.csproj | 2 +- src/Directory.Build.props | 4 +- .../CSharpScriptEngineExtension.cs | 2 - .../ExpressionEvaluatorExtension.cs | 2 - .../Services/Plugin/PluginService.cs | 2 - src/Version.props | 2 +- 104 files changed, 3132 insertions(+), 615 deletions(-) create mode 100644 src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs create mode 100644 src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/HttpVersionAttribute.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/RefererAttribute.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/SuppressExceptionsAttribute.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/RefererDeclarativeExtractor.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpMultipartFormDataBuilderExtensions.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteClient.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpClientOptions.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Attributes/ClayAttribute.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Operator.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Converters/ObjectToClayJsonConverter.cs create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessExtensions.cs diff --git a/src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs b/src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs new file mode 100644 index 000000000..c2472d75b --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; + +using ThingsGateway; + +namespace Microsoft.Extensions.Hosting; + +/// +/// HostApplication 拓展 +/// +public static class AppHostApplicationBuilderExtensions +{ + /// + /// Host 应用注入 + /// + /// Host 应用构建器 + /// + /// HostApplicationBuilder + public static HostApplicationBuilder Inject(this HostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true) + { + // 初始化配置 + InternalApp.ConfigureApplication(hostApplicationBuilder, autoRegisterBackgroundService); + + return hostApplicationBuilder; + } + + /// + /// 注册依赖组件 + /// + /// 派生自 + /// Host 应用构建器 + /// 组件参数 + /// + public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, object options = default) + where TComponent : class, IServiceComponent, new() + { + hostApplicationBuilder.Services.AddComponent(options); + + return hostApplicationBuilder; + } + + /// + /// 注册依赖组件 + /// + /// 派生自 + /// 组件参数 + /// Host 应用构建器 + /// 组件参数 + /// + public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, TComponentOptions options = default) + where TComponent : class, IServiceComponent, new() + { + hostApplicationBuilder.Services.AddComponent(options); + + return hostApplicationBuilder; + } + + /// + /// 注册依赖组件 + /// + /// Host 应用构建器 + /// 组件类型 + /// 组件参数 + /// + public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, Type componentType, object options = default) + { + hostApplicationBuilder.Services.AddComponent(componentType, options); + + return hostApplicationBuilder; + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs b/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs index 3855a3558..386fed9c7 100644 --- a/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs @@ -467,18 +467,20 @@ public static class ObjectExtensions return obj; } + /// /// 查找方法指定特性,如果没找到则继续查找声明类 /// /// /// /// + /// searchFromRuntimeType /// - internal static TAttribute GetFoundAttribute(this MethodInfo method, bool inherit) + internal static TAttribute GetFoundAttribute(this MethodInfo method, bool inherit, bool searchFromReflectedType = false) where TAttribute : Attribute { // 获取方法所在类型 - var declaringType = method.DeclaringType; + var declaringType = !searchFromReflectedType ? method.DeclaringType : method.ReflectedType; // 解决嵌套继承问题 var attributeType = typeof(TAttribute); @@ -493,7 +495,6 @@ public static class ObjectExtensions return foundAttribute; } - /// /// 格式化字符串 /// diff --git a/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs b/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs index 12269624d..22acf03cb 100644 --- a/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs +++ b/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs @@ -132,6 +132,34 @@ internal static class InternalApp }); } + /// + /// 配置 Furion 框架(非 Web) + /// + /// + /// + internal static void ConfigureApplication(IHostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true) + { + // 存储环境对象 + HostEnvironment = hostApplicationBuilder.Environment; + + // 加载配置 + AddJsonFiles(hostApplicationBuilder.Configuration, hostApplicationBuilder.Environment); + + // 存储配置对象 + Configuration = hostApplicationBuilder.Configuration; + + // 存储服务提供器 + InternalServices = hostApplicationBuilder.Services; + + // 存储根服务 + hostApplicationBuilder.Services.AddHostedService(); + + // 初始化应用服务 + hostApplicationBuilder.Services.AddApp(); + + // 自动注册 BackgroundService + if (autoRegisterBackgroundService) hostApplicationBuilder.Services.AddAppHostedService(); + } /// /// 自动装载主机配置 /// diff --git a/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs index e5bcae81e..e727df64c 100644 --- a/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs +++ b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs @@ -9,7 +9,7 @@ // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // ------------------------------------------------------------------------ -using System.Runtime.CompilerServices; + using System.Security.Cryptography; using System.Text; @@ -29,34 +29,36 @@ public class AESEncryption /// 偏移量 /// 模式 /// 填充 + /// /// - public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - var bKey = Encoding.UTF8.GetBytes(skey); + var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey); + if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes."); using var aesAlg = Aes.Create(); aesAlg.Key = bKey; aesAlg.Mode = mode; aesAlg.Padding = padding; - // 如果是 ECB 模式,不需要 IV if (mode != CipherMode.ECB) { - aesAlg.IV = iv ?? aesAlg.IV; // 如果未提供 IV,则使用随机生成的 IV + aesAlg.IV = iv ?? aesAlg.IV; + if (iv != null && iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes."); } - using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + using var encryptor = aesAlg.CreateEncryptor(); using var msEncrypt = new MemoryStream(); using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) - using (var swEncrypt = new StreamWriter(csEncrypt)) + using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8)) { swEncrypt.Write(text); } var encryptedContent = msEncrypt.ToArray(); - // 如果是 CBC 模式,将 IV 和密文拼接在一起 - if (mode != CipherMode.ECB) + // 仅在未提供 IV 时拼接 IV + if (mode != CipherMode.ECB && iv == null) { var result = new byte[aesAlg.IV.Length + encryptedContent.Length]; Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length); @@ -76,35 +78,43 @@ public class AESEncryption /// 偏移量 /// 模式 /// 填充 + /// /// - public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { var fullCipher = Convert.FromBase64String(hash); - - var bKey = Encoding.UTF8.GetBytes(skey); + var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey); + if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes."); using var aesAlg = Aes.Create(); aesAlg.Key = bKey; aesAlg.Mode = mode; aesAlg.Padding = padding; - // 如果是 ECB 模式,不需要 IV if (mode != CipherMode.ECB) { - var bVector = new byte[16]; - var cipher = new byte[fullCipher.Length - bVector.Length]; + if (iv == null) + { + if (fullCipher.Length < aesAlg.BlockSize / 8) throw new ArgumentException("The ciphertext length is insufficient to extract the IV."); - Unsafe.CopyBlock(ref bVector[0], ref fullCipher[0], (uint)bVector.Length); - Unsafe.CopyBlock(ref cipher[0], ref fullCipher[bVector.Length], (uint)(fullCipher.Length - bVector.Length)); - - aesAlg.IV = iv ?? bVector; - fullCipher = cipher; + iv = new byte[aesAlg.BlockSize / 8]; + var cipher = new byte[fullCipher.Length - iv.Length]; + Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length); + Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length); + aesAlg.IV = iv; + fullCipher = cipher; + } + else + { + if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes."); + aesAlg.IV = iv; + } } - using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + using var decryptor = aesAlg.CreateDecryptor(); using var msDecrypt = new MemoryStream(fullCipher); using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); - using var srDecrypt = new StreamReader(csDecrypt); + using var srDecrypt = new StreamReader(csDecrypt, Encoding.UTF8); return srDecrypt.ReadToEnd(); } @@ -117,19 +127,13 @@ public class AESEncryption /// 偏移量 /// 模式 /// 填充 + /// /// 加密后的字节数组 - public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - // 确保密钥长度为 128 位、192 位或 256 位 - var bKey = new byte[32]; // 256 位密钥 - var keyBytes = Encoding.UTF8.GetBytes(skey); - Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length)); - - // 如果是 ECB 模式,不需要 IV - if (mode != CipherMode.ECB) - { - iv ??= GenerateRandomIV(); // 生成随机 IV - } + // 验证密钥长度 + var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey); + if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes."); using var aesAlg = Aes.Create(); aesAlg.Key = bKey; @@ -138,34 +142,29 @@ public class AESEncryption if (mode != CipherMode.ECB) { - aesAlg.IV = iv; + aesAlg.IV = iv ?? GenerateRandomIV(); + if (aesAlg.IV.Length != 16) throw new ArgumentException("The IV length must be 16 bytes."); } using var memoryStream = new MemoryStream(); - using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Write); - - cryptoStream.Write(bytes, 0, bytes.Length); - cryptoStream.FlushFinalBlock(); - - // 如果是 CBC 模式,将 IV 和密文拼接在一起 - if (mode != CipherMode.ECB) + using (var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(), CryptoStreamMode.Write)) { - var result = new byte[aesAlg.IV.Length + memoryStream.ToArray().Length]; + cryptoStream.Write(bytes, 0, bytes.Length); + cryptoStream.FlushFinalBlock(); + } + + var encryptedContent = memoryStream.ToArray(); + + // 仅在未提供 IV 时拼接 IV + if (mode != CipherMode.ECB && iv == null) + { + var result = new byte[aesAlg.IV.Length + encryptedContent.Length]; Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length); - Buffer.BlockCopy(memoryStream.ToArray(), 0, result, aesAlg.IV.Length, memoryStream.ToArray().Length); + Buffer.BlockCopy(encryptedContent, 0, result, aesAlg.IV.Length, encryptedContent.Length); return result; } - // 如果是 ECB 模式,直接返回密文 - return memoryStream.ToArray(); - } - - // 生成随机 IV - private static byte[] GenerateRandomIV() - { - using var aes = Aes.Create(); - aes.GenerateIV(); - return aes.IV; + return encryptedContent; } /// @@ -176,25 +175,13 @@ public class AESEncryption /// 偏移量 /// 模式 /// 填充 + /// /// - public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - // 确保密钥长度为 128 位、192 位或 256 位 - var bKey = new byte[32]; // 256 位密钥 - var keyBytes = Encoding.UTF8.GetBytes(skey); - Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length)); - - // 如果是 ECB 模式,不需要 IV - if (mode != CipherMode.ECB) - { - if (iv == null) - { - // 从密文中提取 IV - iv = new byte[16]; - Array.Copy(bytes, iv, iv.Length); - bytes = bytes.Skip(iv.Length).ToArray(); - } - } + // 验证密钥长度 + var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey); + if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes."); using var aesAlg = Aes.Create(); aesAlg.Key = bKey; @@ -203,21 +190,36 @@ public class AESEncryption if (mode != CipherMode.ECB) { + if (iv == null) + { + // 提取IV + if (bytes.Length < 16) throw new ArgumentException("The ciphertext length is insufficient to extract the IV."); + iv = bytes.Take(16).ToArray(); + bytes = bytes.Skip(16).ToArray(); + } + else + { + if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes."); + } aesAlg.IV = iv; } using var memoryStream = new MemoryStream(bytes); - using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Read); + using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read); using var originalStream = new MemoryStream(); - var buffer = new byte[1024]; - var readBytes = 0; - - while ((readBytes = cryptoStream.Read(buffer, 0, buffer.Length)) > 0) - { - originalStream.Write(buffer, 0, readBytes); - } - + cryptoStream.CopyTo(originalStream); return originalStream.ToArray(); } + + /// + /// 生成随机 IV + /// + /// + private static byte[] GenerateRandomIV() + { + using var aes = Aes.Create(); + aes.GenerateIV(); + return aes.IV; + } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs new file mode 100644 index 000000000..5e8aca415 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using System.IO.Compression; +using System.Text; + +namespace ThingsGateway.DataEncryption; + +/// +/// GZip 压缩解压 +/// +[SuppressSniffer] +public static class GzipEncryption +{ + /// + /// 压缩字符串并返回字节数组 + /// + /// + /// + public static byte[] Compress(string text) + { + var buffer = Encoding.UTF8.GetBytes(text); + + using var ms = new MemoryStream(); + using (var zip = new GZipStream(ms, CompressionMode.Compress, true)) + { + zip.Write(buffer, 0, buffer.Length); + } + + return ms.ToArray(); + } + + /// + /// 从字节数组解压 + /// + /// + /// + public static string Decompress(byte[] bytes) + { + using var ms = new MemoryStream(bytes); + using var zip = new GZipStream(ms, CompressionMode.Decompress); + using var outStream = new MemoryStream(); + + zip.CopyTo(outStream); + + return Encoding.UTF8.GetString(outStream.ToArray()); + } + + /// + /// 压缩字符串并返回 Base64 字符串 + /// + /// + /// + public static string CompressToBase64(string text) + { + var buffer = Encoding.UTF8.GetBytes(text); + + using var ms = new MemoryStream(); + using (var zip = new GZipStream(ms, CompressionMode.Compress, true)) + { + zip.Write(buffer, 0, buffer.Length); + } + + return Convert.ToBase64String(ms.ToArray()); + } + + /// + /// 从 Base64 字符串解压 + /// + /// + /// + public static string DecompressFromBase64(string base64String) + { + var compressedData = Convert.FromBase64String(base64String); + + using var ms = new MemoryStream(compressedData); + using var zip = new GZipStream(ms, CompressionMode.Decompress); + using var outStream = new MemoryStream(); + + zip.CopyTo(outStream); + + return Encoding.UTF8.GetString(outStream.ToArray()); + } +} diff --git a/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs b/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs index bb5251a1d..5b26b9056 100644 --- a/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs @@ -77,10 +77,11 @@ public static class StringEncryptionExtensions /// 偏移量 /// 模式 /// 填充 + /// /// string - public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - return AESEncryption.Encrypt(text, skey, iv, mode, padding); + return AESEncryption.Encrypt(text, skey, iv, mode, padding, isBase64); } /// @@ -91,10 +92,11 @@ public static class StringEncryptionExtensions /// 偏移量 /// 模式 /// 填充 + /// /// string - public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - return AESEncryption.Decrypt(text, skey, iv, mode, padding); + return AESEncryption.Decrypt(text, skey, iv, mode, padding, isBase64); } /// @@ -105,10 +107,11 @@ public static class StringEncryptionExtensions /// 偏移量 /// 模式 /// 填充 + /// /// string - public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - return AESEncryption.Encrypt(bytes, skey, iv, mode, padding); + return AESEncryption.Encrypt(bytes, skey, iv, mode, padding, isBase64); } /// @@ -119,10 +122,11 @@ public static class StringEncryptionExtensions /// 偏移量 /// 模式 /// 填充 + /// /// string - public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false) { - return AESEncryption.Decrypt(bytes, skey, iv, mode, padding); + return AESEncryption.Decrypt(bytes, skey, iv, mode, padding, isBase64); } /// @@ -243,4 +247,44 @@ public static class StringEncryptionExtensions { return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength); } + + /// + /// Gzip 压缩字符串并返回字节数组 + /// + /// + /// + public static byte[] ToGzipCompress(this string text) + { + return GzipEncryption.Compress(text); + } + + /// + /// Gzip 从字节数组解压 + /// + /// + /// + public static string ToGzipDecompress(this byte[] bytes) + { + return GzipEncryption.Decompress(bytes); + } + + /// + /// Gzip 压缩字符串并返回 Base64 字符串 + /// + /// + /// + public static string ToGzipCompressToBase64(this string text) + { + return GzipEncryption.CompressToBase64(text); + } + + /// + /// Gzip 从 Base64 字符串解压 + /// + /// + /// + public static string ToGzipDecompressFromBase64(this string base64String) + { + return GzipEncryption.DecompressFromBase64(base64String); + } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs b/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs index 9c4bbf70e..58ca1f379 100644 --- a/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs +++ b/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs @@ -565,10 +565,10 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase(); // 判断是否贴有任何 [FromXXX] 特性了 - var hasFormAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType())); + var hasFromAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType())); // 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性 - if (isQueryParametersAction && !hasFormAttribute) + if (isQueryParametersAction && !hasFromAttribute) { parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() }); continue; @@ -577,7 +577,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat // 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过 // 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过 if (!parameterAttributes.Any(u => u is FromRouteAttribute) - && (!parameterType.IsRichPrimitive() || hasFormAttribute)) continue; + && (!parameterType.IsRichPrimitive() || hasFromAttribute)) continue; // 处理基元数组数组类型,还有全局配置参数问题 if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray) @@ -588,7 +588,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat // 处理 [ApiController] 特性情况 // https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference - if (!hasFormAttribute && hasApiControllerAttribute) continue; + if (!hasFromAttribute && hasApiControllerAttribute) continue; // 处理默认基元参数绑定方式,若是 query([FromQuery])则跳过 if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query") diff --git a/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs b/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs index ad4ee6417..345b93033 100644 --- a/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs +++ b/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs @@ -84,8 +84,6 @@ internal sealed class DynamicApiRuntimeChangeProvider : IDynamicApiRuntimeChange if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart); } - GC.Collect(); - GC.WaitForPendingFinalizers(); } } diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs b/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs index f0103877e..dda9145e1 100644 --- a/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs +++ b/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs @@ -72,7 +72,7 @@ public sealed class EventBusOptionsBuilder /// /// 是否启用执行完成触发 GC 回收 /// - public bool GCCollect { get; set; } = true; + public bool GCCollect { get; set; } = false; /// /// 是否启用日志记录 diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs index 64566f767..79556a991 100644 --- a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs +++ b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs @@ -10,6 +10,7 @@ // ------------------------------------------------------------------------ using System.Reflection; +using System.Text.Json; namespace ThingsGateway.EventBus; @@ -57,4 +58,31 @@ public abstract class EventHandlerContext /// /// 如果是动态订阅,可能为 null public EventSubscribeAttribute Attribute { get; } + + private static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerOptions.Default) + { + PropertyNameCaseInsensitive = true + }; + /// + /// 获取负载数据 + /// + /// + /// + public T GetPayload() + { + var rawPayload = Source.Payload; + + if (rawPayload is null) + { + return default; + } + else if (rawPayload is JsonElement jsonElement) + { + return JsonSerializer.Deserialize(jsonElement.GetRawText(), JsonSerializerOptions); + } + else + { + return (T)rawPayload; + } + } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs index 38de54585..c32a4d0cd 100644 --- a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs +++ b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs @@ -38,4 +38,18 @@ public sealed class EventHandlerExecutingContext : EventHandlerContext /// 执行前时间 /// public DateTime ExecutingTime { get; internal set; } + + /// + /// 执行结果 + /// + internal object Result { get; private set; } + + /// + /// 设置执行结果 + /// + /// + public void SetResult(object result) + { + Result = result; + } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs b/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs index 0920966b7..39a2ca792 100644 --- a/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs +++ b/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs @@ -42,4 +42,10 @@ public sealed class EventHandlerEventArgs : EventArgs /// 异常信息 /// public Exception Exception { get; internal set; } + + + /// + /// 执行结果 + /// + public object Result { get; internal set; } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs b/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs index 315982978..a4cc8a5fc 100644 --- a/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs +++ b/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs @@ -304,7 +304,10 @@ internal sealed class EventBusHostedService : BackgroundService } // 触发事件处理程序事件 - _eventPublisher.InvokeEvents(new(eventSource, true)); + _eventPublisher.InvokeEvents(new(eventSource, true) + { + Result = eventHandlerExecutingContext.Result + }); } catch (Exception ex) { diff --git a/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs b/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs index 7e1315d33..272261d4a 100644 --- a/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs +++ b/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs @@ -198,8 +198,9 @@ public class JWTEncryption /// 新刷新 Token 有效期(分钟) /// /// + /// 当刷新时触发 /// - public static async Task AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5) + public static async Task AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action onRefreshing = null) { // 如果验证有效,则跳过刷新 if (context.User.Identity.IsAuthenticated) @@ -245,7 +246,11 @@ public class JWTEncryption // 返回新的 Token httpContext.Response.Headers[accessTokenKey] = accessToken; // 返回新的 刷新Token - httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); + var refreshAccessToken = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); ; + httpContext.Response.Headers[xAccessTokenKey] = refreshAccessToken; + + // 调用刷新后回调函数 + onRefreshing?.Invoke(accessToken, refreshAccessToken); // 处理 axios 问题 httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs); diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs index 801d21324..3b418beda 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs @@ -90,7 +90,8 @@ public sealed class ConsoleFormatterExtend : ConsoleFormatter, IDisposable , true , _disableColors , _formatterOptions.WithTraceId - , _formatterOptions.WithStackFrame); + , _formatterOptions.WithStackFrame + , _formatterOptions.FormatProvider); } // 判断是否自定义了日志筛选器,如果是则检查是否符合条件 diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs index aac123fd4..990ed1807 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs @@ -12,6 +12,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; +using System.Globalization; + namespace ThingsGateway.Logging; /// @@ -69,4 +71,10 @@ public sealed class ConsoleFormatterExtendOptions : ConsoleFormatterOptions /// 日志消息内容转换(如脱敏处理) /// public Func MessageProcess { get; set; } + + /// + /// 格式化提供器 + /// + /// + public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs index 6c6ab23a4..a19a53028 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs @@ -118,7 +118,7 @@ public sealed class DatabaseLogger : ILogger // 设置日志消息模板 logMsg.Message = _options.MessageFormat != null ? _options.MessageFormat(logMsg) - : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame); + : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider); // 空检查 if (logMsg.Message is null) diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs index a3b4511aa..3bddb8cd0 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs @@ -11,6 +11,8 @@ using Microsoft.Extensions.Logging; +using System.Globalization; + namespace ThingsGateway.Logging; /// @@ -80,4 +82,10 @@ public sealed class DatabaseLoggerOptions /// 日志消息内容转换(如脱敏处理) /// public Func MessageProcess { get; set; } + + /// + /// 格式化提供器 + /// + /// + public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs index b50a9a455..5870a4cc7 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs @@ -116,7 +116,7 @@ public sealed class FileLogger : ILogger // 设置日志消息模板 logMsg.Message = _options.MessageFormat != null ? _options.MessageFormat(logMsg) - : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame); + : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider); // 空检查 if (logMsg.Message is null) diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs index 5d9291e88..755e29471 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs @@ -11,6 +11,8 @@ using Microsoft.Extensions.Logging; +using System.Globalization; + namespace ThingsGateway.Logging; /// @@ -104,4 +106,10 @@ public sealed class FileLoggerOptions /// 日志消息内容转换(如脱敏处理) /// public Func MessageProcess { get; set; } + + /// + /// 格式化提供器 + /// + /// + public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs index 20da875fa..b66d27aa4 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs @@ -11,6 +11,8 @@ using Microsoft.Extensions.Logging; +using System.Globalization; + namespace ThingsGateway.Logging; /// @@ -120,6 +122,6 @@ public struct LogMessage /// public override readonly string ToString() { - return Penetrates.OutputStandardMessage(this); + return Penetrates.OutputStandardMessage(this, provider: CultureInfo.InvariantCulture); } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs index 8de7cd80d..4a80674d3 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs @@ -192,7 +192,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs /// /// /// - private static List GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization) + private List GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization) { var templates = new List(); @@ -219,7 +219,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs var succeed = long.TryParse(value, out var seconds); if (succeed) { - value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime():yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd} L)"; + value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd", Settings.FormatProvider)} L)"; } } diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs index d83770ca7..f4d37b0f2 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using System.Globalization; using System.Text.Encodings.Web; using System.Text.Json; @@ -143,4 +144,11 @@ public sealed class LoggingMonitorSettings Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, SkipValidation = true }; + + + /// + /// 格式化提供器 + /// + /// + public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs b/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs index 5f5cdcb27..07a582e45 100644 --- a/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs +++ b/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs @@ -107,13 +107,15 @@ internal static class Penetrates /// /// /// + /// /// internal static string OutputStandardMessage(LogMessage logMsg , string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd" , bool isConsole = false , bool disableColors = true , bool withTraceId = false - , bool withStackFrame = false) + , bool withStackFrame = false + , IFormatProvider? provider = null) { // 空检查 if (logMsg.Message is null) return null; @@ -127,7 +129,7 @@ internal static class Penetrates _ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors); formatString.Append(": "); - formatString.Append(logMsg.LogDateTime.ToString(dateFormat)); + formatString.Append(logMsg.LogDateTime.ToString(dateFormat, provider)); formatString.Append(' '); formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L"); formatString.Append(' '); diff --git a/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs b/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs index 0ad895701..44ffb67c5 100644 --- a/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs +++ b/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs @@ -78,9 +78,9 @@ public partial interface ISchedulerFactory /// IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context); - /// - /// GC 垃圾回收器回收处理 - /// - /// 避免频繁 GC 回收 - void GCCollect(); + ///// + ///// GC 垃圾回收器回收处理 + ///// + ///// 避免频繁 GC 回收 + //void GCCollect(); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs b/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs index 7f3ccfab8..bca173509 100644 --- a/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs +++ b/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs @@ -183,9 +183,10 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory // 标记当前方法初始化完成 PreloadCompleted = true; - // 释放引用内存并立即回收GC + // 释放引用内存 _schedulerBuilders.Clear(); - GCCollect(); + + //GCCollect(); // 输出作业调度器初始化日志 if (!preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count); @@ -393,22 +394,22 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory return jobHandler; } - /// - /// GC 垃圾回收器回收处理 - /// - /// 避免频繁 GC 回收 - public void GCCollect() - { - var nowTime = DateTime.UtcNow; - if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS)) - { - LastGCCollectTime = nowTime; + ///// + ///// GC 垃圾回收器回收处理 + ///// + ///// 避免频繁 GC 回收 + //public void GCCollect() + //{ + // var nowTime = DateTime.UtcNow; + // if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS)) + // { + // LastGCCollectTime = nowTime; - // 通知 GC 垃圾回收器立即回收 - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - } + // // 通知 GC 垃圾回收器立即回收 + // GC.Collect(); + // GC.WaitForPendingFinalizers(); + // } + //} /// /// 释放非托管资源 @@ -535,7 +536,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory //_logger.LogWarning("Schedule hosted service cancels hibernation."); // 通知 GC 垃圾回收器立即回收 - GCCollect(); + //GCCollect(); }); } diff --git a/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs b/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs index 5f98ffd7f..d40b4ef89 100644 --- a/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs +++ b/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs @@ -389,7 +389,7 @@ internal sealed class ScheduleHostedService : BackgroundService _jobCancellationToken.Cancel(jobId, triggerId, false); // 通知 GC 垃圾回收器回收 - _schedulerFactory.GCCollect(); + //_schedulerFactory.GCCollect(); } }, stoppingToken); }); diff --git a/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs b/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs index 465766fde..df10d78cf 100644 --- a/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs +++ b/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs @@ -113,10 +113,8 @@ public static class SpecificationDocumentBuilder } // 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口 - var apiExplorerSettings = method.GetFoundAttribute(true); - - var apiDescriptionSettings = method.GetFoundAttribute(true); - + var apiExplorerSettings = method.GetFoundAttribute(true, true); + var apiDescriptionSettings = method.GetFoundAttribute(true, true); if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false; if (currentGroup == AllGroupsKey) diff --git a/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs b/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs index 40267f34e..9898ffddb 100644 --- a/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs +++ b/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs @@ -433,10 +433,15 @@ public partial class Crontab { newValue = newValue.AddSeconds(-newValue.Second); } - + // 初始化是否存在随机 R 标识符 + var randomSecond = false; + var randomMinute = false; + var randomHour = false; // 获取分钟、小时所有字符解析器 var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast().ToList(); + randomMinute = minuteParsers.OfType().Any(); var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast().ToList(); + randomHour = hourParsers.OfType().Any(); // 获取秒、分钟、小时解析器中最小起始值 // 该值主要用来获取下一个发生值的输入参数 @@ -456,7 +461,7 @@ public partial class Crontab { // 获取秒所有字符解析器 var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast().ToList(); - + randomSecond = secondParsers.OfType().Any(); // 获取秒解析器最小起始值 firstSecondValue = secondParsers.Select(x => x.First()).Min(); @@ -519,8 +524,8 @@ public partial class Crontab // 设置起始时间为下一个小时时间 newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours, - overflow ? firstMinuteValue : newMinutes, - overflow ? firstSecondValue : newSeconds); + overflow && !randomMinute ? firstMinuteValue : newMinutes, + overflow && !randomSecond ? firstSecondValue : newSeconds); // 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器 if (!overflow && !IsMatch(newValue)) @@ -534,7 +539,7 @@ public partial class Crontab } // 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间 - if (!overflow) + if (!randomHour && !overflow) { return MinDate(newValue, endTime); } @@ -788,8 +793,15 @@ public partial class Crontab /// 默认值 /// 控制秒、分钟、小时到达59秒/分和23小时开关 /// - private static int Increment(IEnumerable parsers, int value, int defaultValue, out bool overflow) + private static int Increment(List parsers, int value, int defaultValue, out bool overflow) { + // 检查是否是随机 R 字符解析器 + if (parsers.Count == 1 && parsers.First() is RandomParser randomParser) + { + overflow = true; + return randomParser.Next(value).Value; + } + var nextValue = parsers.Select(x => x.Next(value)) .Where(x => x > value) .Min() @@ -808,7 +820,7 @@ public partial class Crontab /// 默认值 /// 控制秒、分钟、小时到达59秒/分和23小时开关 /// - private static int Decrement(IEnumerable parsers, int value, int defaultValue, out bool overflow) + private static int Decrement(List parsers, int value, int defaultValue, out bool overflow) { var previousValue = parsers.Select(x => x.Previous(value)) .Where(x => x < value) diff --git a/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs b/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs index fec69198d..7c12501bb 100644 --- a/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs +++ b/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs @@ -69,7 +69,7 @@ internal sealed class RandomParser : ICronParser, ITimeParser /// public bool IsMatch(DateTime datetime) { - return true; + return Kind is not CrontabFieldKind.Hour; } /// diff --git a/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs b/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs index ad0783e3c..7e1c30922 100644 --- a/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs +++ b/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs @@ -168,7 +168,7 @@ public static class UnifyContext if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null; // 获取序列化配置 - var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute(true); + var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute(true, true); if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null; // 解析全局配置 @@ -225,7 +225,8 @@ public static class UnifyContext || method.GetRealReturnType().HasImplementedRawGeneric(unityMetadata.ResultType) || method.CustomAttributes.Any(x => typeof(NonUnifyAttribute).IsAssignableFrom(x.AttributeType) || typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType)) || method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true) - || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData"); + || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData") + || method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>); if (!isWebRequest) { @@ -255,7 +256,8 @@ public static class UnifyContext !method.CustomAttributes.Any(x => typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType)) && method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true) ) - || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData"); + || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData") + || method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>); unifyResult = isSkip ? null : App.RootServices.GetService(unityMetadata.ProviderType) as IUnifyResultProvider; return unifyResult == null || isSkip; @@ -398,7 +400,7 @@ public static class UnifyContext { if (method == default) return default; - var unityProviderAttribute = method.GetFoundAttribute(true); + var unityProviderAttribute = method.GetFoundAttribute(true, true); // 获取元数据 var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata); diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs new file mode 100644 index 000000000..c04e1dd5e --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ThingsGateway.Converters.Json; + +/// +/// JSON 序列化转换器 +/// +/// 在不符合 ISO 8601-1:2019 格式的 时间使用 DateTime.Parse 作为回退。 +public sealed class DateTimeConverterUsingDateTimeParseAsFallback : JsonConverter +{ + /// + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // 尝试获取 ISO 8601-1:2019 格式时间 + if (!reader.TryGetDateTime(out var value)) + { + value = DateTime.Parse(reader.GetString()!); + } + + return value; + } + + /// + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value); +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs new file mode 100644 index 000000000..2a4c0599c --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ThingsGateway.Converters.Json; + +/// +/// JSON 序列化转换器 +/// +/// 在不符合 ISO 8601-1:2019 格式的 时间使用 DateTimeOffset.Parse 作为回退。 +public sealed class DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback : JsonConverter +{ + /// + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // 尝试获取 ISO 8601-1:2019 格式时间 + if (!reader.TryGetDateTimeOffset(out var value)) + { + value = DateTimeOffset.Parse(reader.GetString()!); + } + + return value; + } + + /// + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value); +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs new file mode 100644 index 000000000..3efcc9512 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; + +using ThingsGateway.Extensions; + +namespace ThingsGateway.Converters.Json; + +/// +/// JSON 序列化转换器 +/// +/// 解决 Number 类型和 Boolean 类型转 String 类型时异常。 +public sealed class StringJsonConverter : JsonConverter +{ + /// + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.TokenType switch + { + JsonTokenType.True or JsonTokenType.False => reader.GetBoolean().ToString(), + JsonTokenType.Number => reader.ConvertRawValueToString(), + _ => reader.GetString() + }; + + /// + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => + writer.WriteStringValue(value); +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs index c704e5164..3d53182b0 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs @@ -10,6 +10,7 @@ // ------------------------------------------------------------------------ using System.Linq.Expressions; +using System.Reflection; namespace ThingsGateway.Extensions; @@ -43,7 +44,7 @@ internal static class LinqExpressionExtensions } /// - /// 解析表达式属性名称 + /// 解析表达式并获取属性的 实例 /// /// 对象类型 /// 属性类型 @@ -51,48 +52,54 @@ internal static class LinqExpressionExtensions /// /// /// - /// + /// /// /// - internal static string GetPropertyName(this Expression> propertySelector) => + internal static PropertyInfo GetProperty(this Expression> propertySelector) => propertySelector.Body switch { // 检查 Lambda 表达式的主体是否是 MemberExpression 类型 - MemberExpression memberExpression => GetPropertyName(memberExpression), - + MemberExpression memberExpression => GetProperty(memberExpression), // 如果主体是 UnaryExpression 类型,则继续解析 - UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetPropertyName( + UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetProperty( nestedMemberExpression), - - _ => throw new ArgumentException("Expression is not valid for property selection.") + _ => throw new ArgumentException("Expression must be a simple member access (e.g. x => x.Property).", + nameof(propertySelector)) }; /// - /// 解析表达式属性名称 + /// 从成员表达式中提取 实例 /// - /// 对象类型 /// /// /// + /// 对象类型 /// - /// + /// /// /// - internal static string GetPropertyName(MemberExpression memberExpression) + internal static PropertyInfo GetProperty(MemberExpression memberExpression) { // 空检查 ArgumentNullException.ThrowIfNull(memberExpression); - // 获取属性声明类型 - var propertyType = memberExpression.Member.DeclaringType; - - // 检查是否越界访问属性 - if (propertyType != typeof(T)) + // 确保表达式根是 T 类型的参数 + if (memberExpression.Expression is not ParameterExpression parameterExpression || + parameterExpression.Type != typeof(T)) { - throw new ArgumentException("Invalid property selection."); + throw new ArgumentException( + $"Expression '{memberExpression}' must refer to a member of type '{typeof(T)}'.", + nameof(memberExpression)); } - // 返回属性名称 - return memberExpression.Member.Name; + // 确保成员是属性(非字段) + if (memberExpression.Member is not PropertyInfo propertyInfo) + { + throw new ArgumentException( + $"Expression '{memberExpression}' refers to a field. Only properties are supported.", + nameof(memberExpression)); + } + + return propertyInfo; } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs index 528f332fc..0aba62fa3 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Text; @@ -149,7 +150,7 @@ internal static partial class StringExtensions var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators); return (from pair in pairs - select pair.Split('=') + select pair.Split('=', 2) // 限制只分割一次 into keyValue where keyValue.Length == 2 select new KeyValuePair(keyValue[0].Trim(), keyValue[1])).ToList(); @@ -328,6 +329,18 @@ internal static partial class StringExtensions }); } + /// + /// 转换输入字符串中的任何转义字符 + /// + /// + /// + /// + /// + /// + /// + internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) => + string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input); + /// /// 占位符匹配正则表达式 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs index 8c6180782..d2871015f 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs @@ -9,6 +9,8 @@ // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // ------------------------------------------------------------------------ +using System.Buffers; +using System.Text; using System.Text.Json; namespace ThingsGateway.Extensions; @@ -34,4 +36,17 @@ internal static class Utf8JsonReaderExtensions return jsonDocument.RootElement.Clone().GetRawText(); } + + /// + /// 从 中提取原始值,并将其转换为字符串 + /// + /// 支持处理各种类型的原始值(例如数字、布尔值等)。 + /// + /// + /// + /// + /// + /// + internal static string ConvertRawValueToString(this Utf8JsonReader reader) => + Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs index 65a51e8ed..a3cff3a2e 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs @@ -97,16 +97,45 @@ internal static class V5_ObjectExtensions case ICollection collection: count = collection.Count; return true; + // 检查对象是否实现了 IEnumerable 接口 + case IEnumerable enumerable: + // 获取集合枚举数 + var enumerator = enumerable.GetEnumerator(); + + try + { + // 检查枚举数是否可以推进到下一个元素 + if (!enumerator.MoveNext()) + { + count = 0; + return true; + } + + // 枚举数循环推进到下一个元素并叠加推进次数 + var c = 1; + while (enumerator.MoveNext()) + { + c++; + } + + count = c; + return true; + } + finally + { + // 检查枚举数是否实现了 IDisposable 接口 + if (enumerator is IDisposable disposable) + { + disposable.Dispose(); + } + } } // 反射查找是否存在 Count 属性 - var runtimeProperty = obj.GetType() - .GetRuntimeProperty("Count"); + var runtimeProperty = obj.GetType().GetRuntimeProperty("Count"); // 反射获取 Count 属性值 - if (runtimeProperty is not null - && runtimeProperty.CanRead - && runtimeProperty.PropertyType == typeof(int)) + if (runtimeProperty is not null && runtimeProperty.CanRead && runtimeProperty.PropertyType == typeof(int)) { count = (int)runtimeProperty.GetValue(obj)!; return true; diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs index 6bd6d7a22..049946203 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs @@ -38,7 +38,7 @@ public sealed class HttpContextForwardBuilder /// /// 忽略在转发时需要跳过的请求标头列表 /// - internal static HashSet _ignoreRequestHeaders = + internal static readonly HashSet _ignoreRequestHeaders = [ Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding", "Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges" @@ -356,8 +356,7 @@ public sealed class HttpContextForwardBuilder if (multipartSection.AsFileSection() is not null) { // 复制多部分表单内容文件节内容 - await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, httpRequestBuilder, - cancellationToken).ConfigureAwait(false); + await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, cancellationToken).ConfigureAwait(false); } else { @@ -410,15 +409,11 @@ public sealed class HttpContextForwardBuilder /// /// /// - /// - /// - /// /// /// /// internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection, - HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, HttpRequestBuilder httpRequestBuilder, - CancellationToken cancellationToken) + HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, CancellationToken cancellationToken) { // 初始化 MemoryStream 实例 var memoryStream = new MemoryStream(); @@ -433,10 +428,8 @@ public sealed class HttpContextForwardBuilder var fileMultipartSection = multipartSection.AsFileSection()!; // 添加文件流 - httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName); - - // 添加文件流到请求结束时需要释放的集合中 - httpRequestBuilder.AddDisposable(memoryStream); + httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName, + disposeStreamOnRequestCompletion: true); } /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs index 28af3b58a..f119cf20b 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs @@ -124,12 +124,9 @@ public sealed class HttpMultipartFormDataBuilder /// /// /// - public HttpMultipartFormDataBuilder AddJson(object rawJson, string? name = null, Encoding? contentEncoding = null, + public HttpMultipartFormDataBuilder AddJson(object? rawJson, string? name = null, Encoding? contentEncoding = null, string? contentType = null) { - // 空检查 - ArgumentNullException.ThrowIfNull(rawJson); - // 检查是否配置表单名或不是字符串类型 if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString) { @@ -292,10 +289,8 @@ public sealed class HttpMultipartFormDataBuilder // 从互联网 URL 地址中加载流 var fileStream = Helpers.GetStreamFromRemote(url); - // 添加文件流到请求结束时需要释放的集合中 - _httpRequestBuilder.AddDisposable(fileStream); - - return AddStream(fileStream, name, newFileName, contentType, contentEncoding); + return AddStream(fileStream, name, newFileName, contentType, contentEncoding, + true); } /// @@ -365,10 +360,8 @@ public sealed class HttpMultipartFormDataBuilder // 读取文件流(没有 using) var fileStream = File.OpenRead(filePath); - // 添加文件流到请求结束时需要释放的集合中 - _httpRequestBuilder.AddDisposable(fileStream); - - return AddStream(fileStream, name, newFileName, contentType, contentEncoding); + return AddStream(fileStream, name, newFileName, contentType, contentEncoding, + true); } /// @@ -407,10 +400,8 @@ public sealed class HttpMultipartFormDataBuilder // 初始化带读写进度的文件流 var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName); - // 添加文件流到请求结束时需要释放的集合中 - _httpRequestBuilder.AddDisposable(progressFileStream); - - return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding); + return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding, + true); } /// @@ -500,11 +491,12 @@ public sealed class HttpMultipartFormDataBuilder /// 文件的名称 /// 内容类型 /// 内容编码 + /// 是否在请求结束后自动释放流。默认值为:false /// /// /// public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null, - string? contentType = null, Encoding? contentEncoding = null) + string? contentType = null, Encoding? contentEncoding = null, bool disposeStreamOnRequestCompletion = false) { // 空检查 ArgumentNullException.ThrowIfNull(stream); @@ -529,6 +521,12 @@ public sealed class HttpMultipartFormDataBuilder FileName = fileName }); + // 是否在请求结束后自动释放流 + if (disposeStreamOnRequestCompletion) + { + _httpRequestBuilder.AddDisposable(stream); + } + return this; } @@ -697,6 +695,20 @@ public sealed class HttpMultipartFormDataBuilder return this; } + /// + /// 设置是否移除默认的多部分内容的 Content-Type + /// + /// 如果为 true 则移除,默认为 false + /// + /// + /// + public HttpMultipartFormDataBuilder SetOmitContentType(bool omit) + { + OmitContentType = omit; + + return this; + } + /// /// 构建 实例 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs index c557acab0..bb13838e9 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs @@ -327,23 +327,20 @@ public sealed partial class HttpRequestBuilder /// 键 /// 值 /// 是否转义字符串,默认 false + /// 是否替换已存在的请求标头。默认值为 false /// /// /// - /// - /// - /// - /// 是否替换已存在的请求标头。默认值为 false。 /// /// /// - public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, CultureInfo? culture = null, - IEqualityComparer? comparer = null, bool replace = false) + public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, bool replace = false, + CultureInfo? culture = null) { // 空检查 ArgumentException.ThrowIfNullOrWhiteSpace(key); - return WithHeaders(new Dictionary { { key, value } }, escape, culture, comparer, replace); + return WithHeaders(new Dictionary { { key, value } }, escape, replace, culture); } /// @@ -352,25 +349,22 @@ public sealed partial class HttpRequestBuilder /// 支持多次调用。 /// 请求标头集合 /// 是否转义字符串,默认 false + /// 是否替换已存在的请求标头。默认值为 false /// /// /// - /// - /// - /// - /// 是否替换已存在的请求标头。默认值为 false。 /// /// /// public HttpRequestBuilder WithHeaders(IDictionary headers, bool escape = false, - CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false) + bool replace = false, CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(headers); // 初始化请求标头 - Headers ??= new Dictionary>(comparer); - var objectHeaders = new Dictionary>(comparer); + Headers ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + var objectHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); // 存在则合并否则添加 objectHeaders.AddOrUpdate(Headers.ToDictionary(u => u.Key, object? (u) => u.Value), false); @@ -380,7 +374,7 @@ public sealed partial class HttpRequestBuilder Headers = objectHeaders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(u => u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(), - comparer); + StringComparer.OrdinalIgnoreCase); return this; } @@ -391,26 +385,23 @@ public sealed partial class HttpRequestBuilder /// 支持多次调用。 /// 请求标头源对象 /// 是否转义字符串,默认 false + /// 是否替换已存在的请求标头。默认值为 false /// /// /// - /// - /// - /// - /// 是否替换已存在的请求标头。默认值为 false。 /// /// /// - public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, CultureInfo? culture = null, - IEqualityComparer? comparer = null, bool replace = false) + public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, bool replace = false, + CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(headerSource); return WithHeaders( headerSource.ObjectToDictionary()!.ToDictionary( - u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture, - comparer, replace); + u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, replace, + culture); } /// @@ -474,6 +465,7 @@ public sealed partial class HttpRequestBuilder public HttpRequestBuilder SetTimeout(TimeSpan timeout) { Timeout = timeout; + TimeoutAction = null; return this; } @@ -494,6 +486,43 @@ public sealed partial class HttpRequestBuilder } Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds); + TimeoutAction = null; + + return this; + } + + /// + /// 设置超时时间 + /// + /// 超时时间 + /// 超时发生时要执行的操作 + /// + /// + /// + public HttpRequestBuilder SetTimeout(TimeSpan timeout, Action onTimeout) + { + // 空检查 + ArgumentNullException.ThrowIfNull(onTimeout); + + SetTimeout(timeout).TimeoutAction = onTimeout; + + return this; + } + + /// + /// 设置超时时间 + /// + /// 超时时间(毫秒) + /// 超时发生时要执行的操作 + /// + /// + /// + public HttpRequestBuilder SetTimeout(double timeoutMilliseconds, Action onTimeout) + { + // 空检查 + ArgumentNullException.ThrowIfNull(onTimeout); + + SetTimeout(timeoutMilliseconds).TimeoutAction = onTimeout; return this; } @@ -570,26 +599,22 @@ public sealed partial class HttpRequestBuilder /// 键 /// 值 /// 是否转义字符串,默认 false + /// 是否替换已存在的查询参数。默认值为 false + /// 是否忽略空值。默认值为 false /// /// /// - /// - /// - /// - /// 是否替换已存在的查询参数。默认值为 false。 - /// 是否忽略空值。默认值为 false。 /// /// /// - public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, - CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false, - bool ignoreNullValues = false) + public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, bool replace = false, + bool ignoreNullValues = false, CultureInfo? culture = null) { // 空检查 ArgumentException.ThrowIfNullOrWhiteSpace(key); - return WithQueryParameters(new Dictionary { { key, value } }, escape, culture, comparer, - replace, ignoreNullValues); + return WithQueryParameters(new Dictionary { { key, value } }, escape, replace, + ignoreNullValues, culture); } /// @@ -598,27 +623,23 @@ public sealed partial class HttpRequestBuilder /// 支持多次调用。 /// 查询参数集合 /// 是否转义字符串,默认 false + /// 是否替换已存在的查询参数。默认值为 false + /// 是否忽略空值。默认值为 false /// /// /// - /// - /// - /// - /// 是否替换已存在的查询参数。默认值为 false。 - /// 是否忽略空值。默认值为 false。 /// /// /// public HttpRequestBuilder WithQueryParameters(IDictionary parameters, bool escape = false, - CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false, - bool ignoreNullValues = false) + bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(parameters); // 初始化查询参数 - QueryParameters ??= new Dictionary>(comparer); - var objectQueryParameters = new Dictionary>(comparer); + QueryParameters ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + var objectQueryParameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); // 存在则合并否则添加 objectQueryParameters.AddOrUpdate(QueryParameters.ToDictionary(u => u.Key, object? (u) => u.Value), false); @@ -629,7 +650,7 @@ public sealed partial class HttpRequestBuilder QueryParameters = objectQueryParameters.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(u => u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(), - comparer); + StringComparer.OrdinalIgnoreCase); return this; } @@ -641,20 +662,16 @@ public sealed partial class HttpRequestBuilder /// 查询参数集合 /// 参数前缀。对于对象类型可生成如 prefix.Name=furionprefix.Age=30 参数格式。 /// 是否转义字符串,默认 false + /// 是否替换已存在的查询参数。默认值为 false + /// 是否忽略空值。默认值为 false /// /// /// - /// - /// - /// - /// 是否替换已存在的查询参数。默认值为 false。 - /// 是否忽略空值。默认值为 false。 /// /// /// public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false, - CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false, - bool ignoreNullValues = false) + bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(parameterSource); @@ -663,7 +680,7 @@ public sealed partial class HttpRequestBuilder parameterSource.ObjectToDictionary()!.ToDictionary( u => $"{(string.IsNullOrWhiteSpace(prefix) ? null : $"{prefix}.")}{u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!}", - u => u.Value), escape, culture, comparer, replace, ignoreNullValues); + u => u.Value), escape, replace, ignoreNullValues, culture); } /// @@ -709,19 +726,16 @@ public sealed partial class HttpRequestBuilder /// /// /// - /// - /// - /// /// /// /// public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false, - CultureInfo? culture = null, IEqualityComparer? comparer = null) + CultureInfo? culture = null) { // 空检查 ArgumentException.ThrowIfNullOrWhiteSpace(key); - return WithPathParameters(new Dictionary { { key, value } }, escape, culture, comparer); + return WithPathParameters(new Dictionary { { key, value } }, escape, culture); } /// @@ -733,26 +747,21 @@ public sealed partial class HttpRequestBuilder /// /// /// - /// - /// - /// /// /// /// - public HttpRequestBuilder WithPathParameters(IDictionary parameters, - bool escape = false, - CultureInfo? culture = null, - IEqualityComparer? comparer = null) + public HttpRequestBuilder WithPathParameters(IDictionary parameters, bool escape = false, + CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(parameters); - PathParameters ??= new Dictionary(comparer); + PathParameters ??= new Dictionary(StringComparer.OrdinalIgnoreCase); // 存在则更新否则添加 PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key, u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape), - comparer)); + StringComparer.OrdinalIgnoreCase)); return this; } @@ -767,15 +776,11 @@ public sealed partial class HttpRequestBuilder /// /// /// - /// - /// - /// /// /// /// public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false, - CultureInfo? culture = null, - IEqualityComparer? comparer = null) + CultureInfo? culture = null) { // 检查是否设置了模板字符串前缀 if (string.IsNullOrWhiteSpace(prefix)) @@ -786,7 +791,7 @@ public sealed partial class HttpRequestBuilder return WithPathParameters( parameterSource.ObjectToDictionary()!.ToDictionary( u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, - culture, comparer); + culture); } ObjectPathParameters ??= new Dictionary(); @@ -823,19 +828,15 @@ public sealed partial class HttpRequestBuilder /// /// /// - /// - /// - /// /// /// /// - public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null, - IEqualityComparer? comparer = null) + public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null) { // 空检查 ArgumentException.ThrowIfNullOrWhiteSpace(key); - return WithCookies(new Dictionary { { key, value } }, escape, culture, comparer); + return WithCookies(new Dictionary { { key, value } }, escape, culture); } /// @@ -847,26 +848,21 @@ public sealed partial class HttpRequestBuilder /// /// /// - /// - /// - /// /// /// /// - public HttpRequestBuilder WithCookies(IDictionary cookies, - bool escape = false, - CultureInfo? culture = null, - IEqualityComparer? comparer = null) + public HttpRequestBuilder WithCookies(IDictionary cookies, bool escape = false, + CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(cookies); - Cookies ??= new Dictionary(comparer); + Cookies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); // 存在则更新否则添加 Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key, u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape), - comparer)); + StringComparer.OrdinalIgnoreCase)); return this; } @@ -880,15 +876,10 @@ public sealed partial class HttpRequestBuilder /// /// /// - /// - /// - /// /// /// /// - public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, - CultureInfo? culture = null, - IEqualityComparer? comparer = null) + public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, CultureInfo? culture = null) { // 空检查 ArgumentNullException.ThrowIfNull(cookieSource); @@ -896,8 +887,7 @@ public sealed partial class HttpRequestBuilder // 存在则更新否则添加 return WithCookies( cookieSource.ObjectToDictionary()!.ToDictionary( - u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture, - comparer); + u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture); } /// @@ -1193,6 +1183,17 @@ public sealed partial class HttpRequestBuilder return this; } + /// + /// 设置身份验证凭据请求授权标头 + /// + /// 身份验证的方案 + /// 身份验证的凭证 + /// + /// + /// + public HttpRequestBuilder AddAuthentication(string scheme, string? parameter) => + AddAuthentication(new AuthenticationHeaderValue(scheme, parameter)); + /// /// 设置身份验证凭据请求授权标头 /// @@ -1328,6 +1329,17 @@ public sealed partial class HttpRequestBuilder ReleaseDisposables(); } + /// + /// 设置请求来源地址 + /// + /// 设置此配置后,将在单次请求标头中添加 Referer 标头。 + /// 请求来源地址,当设置为 "{BASE_ADDRESS}" 时将替换为基地址 + /// + /// + /// + public HttpRequestBuilder SetReferer(string? referer) => + WithHeader(HeaderNames.Referer, referer, replace: true); + /// /// 设置模拟浏览器环境 /// @@ -1364,6 +1376,17 @@ public sealed partial class HttpRequestBuilder public HttpRequestBuilder WithAnyStatusCodeHandler(Func handler) => WithStatusCodeHandler(["*"], handler); + /// + /// 添加请求成功(200-299)状态码处理程序 + /// + /// 自定义处理程序 + /// + /// + /// + public HttpRequestBuilder + WithSuccessStatusCodeHandler(Func handler) => + WithStatusCodeHandler("200-299", handler); + /// /// 添加状态码处理程序 /// @@ -1590,6 +1613,107 @@ public sealed partial class HttpRequestBuilder ? null : new Uri(baseAddress, UriKind.RelativeOrAbsolute)); + /// + /// 设置 HTTP 版本 + /// + /// 版本号 + /// + /// + /// + public HttpRequestBuilder SetVersion(string? version) => + SetVersion(string.IsNullOrWhiteSpace(version) ? null : new Version(version)); + + /// + /// 设置 HTTP 版本 + /// + /// + /// + /// + /// + /// + /// + public HttpRequestBuilder SetVersion(Version? version) + { + Version = version; + + return this; + } + + /// + /// 设置异常抑制 + /// + /// 抑制所有异常。重复调用仅最后一次调用生效。 + /// + /// + /// + public HttpRequestBuilder SuppressExceptions() => SuppressExceptions(true); + + /// + /// 设置异常抑制 + /// + /// 重复调用仅最后一次调用生效。 + /// 是否启用异常抑制。当设置为 false 时,将禁用异常抑制机制。 + /// + /// + /// + public HttpRequestBuilder SuppressExceptions(bool enable) => SuppressExceptions(enable ? [typeof(Exception)] : []); + + /// + /// 设置是否移除默认的内容的 Content-Type + /// + /// 如果为 true 则移除,默认为 false + /// + /// + /// + public HttpRequestBuilder SetOmitContentType(bool omit) + { + OmitContentType = omit; + + return this; + } + + /// + /// 设置异常抑制 + /// + /// 重复调用仅最后一次调用生效。 + /// 异常抑制类型集合 + /// + /// + /// + /// + public HttpRequestBuilder SuppressExceptions(Type[] exceptionTypes) + { + // 空检查 + ArgumentNullException.ThrowIfNull(exceptionTypes); + + // 检查是否包含 null 或者不是 Exception 类型的元素 + if (exceptionTypes.Any(u => (Type?)u is null || !typeof(Exception).IsAssignableFrom(u))) + { + throw new ArgumentException( + "All elements in exceptionTypes must be non-null and assignable to System.Exception."); + } + + // 释放引用(无关紧要) + SuppressExceptionTypes = null; + + // 空检查 + if (exceptionTypes.Length == 0) + { + return this; + } + + // 确保每次都能覆盖 + SuppressExceptionTypes = []; + + // 遍历异常抑制类型集合逐条追加 + foreach (var exceptionType in exceptionTypes) + { + SuppressExceptionTypes.Add(exceptionType); + } + + return this; + } + /// /// 释放可释放的对象集合 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs index 20cab95c7..8a2919ff1 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs @@ -157,6 +157,11 @@ public sealed partial class HttpRequestBuilder /// public Uri? BaseAddress { get; private set; } + /// + /// HTTP 版本 + /// + public Version? Version { get; private set; } + /// /// 实例提供器 /// @@ -181,7 +186,7 @@ public sealed partial class HttpRequestBuilder /// /// 用于处理在设置 的请求消息的内容时的操作 /// - public Action? OnPreSetContent { get; private set; } + public Action? OnPreSetContent { get; private set; } /// /// 用于处理在发送 HTTP 请求之前的操作 @@ -201,7 +206,13 @@ public sealed partial class HttpRequestBuilder /// /// /// - internal HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; } + public HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; } + + /// + /// 是否移除默认的内容的 Content-Type + /// + /// 默认值为:false + public bool OmitContentType { get; private set; } /// /// 如果 HTTP 响应的 IsSuccessStatusCode 属性是 false,则引发异常。 @@ -273,4 +284,15 @@ public sealed partial class HttpRequestBuilder get; private set; } + + /// + /// 异常抑制类型集合 + /// + /// 当配置了异常抑制类型集合后,框架将抑制(即不抛出)该集合中匹配的异常类型。 + internal HashSet? SuppressExceptionTypes { get; private set; } + + /// + /// 超时发生时要执行的操作 + /// + internal Action? TimeoutAction { get; private set; } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs index 4f4dd1d0d..8f34b034e 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs @@ -10,6 +10,9 @@ // ------------------------------------------------------------------------ using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; namespace ThingsGateway.HttpRemote; @@ -614,4 +617,116 @@ public sealed partial class HttpRequestBuilder /// public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) => new(method, args); + + /// + /// 从 JSON 中创建 实例 + /// + /// JSON 字符串 + /// 自定义配置委托 + /// + /// + /// + /// + /// + public static HttpRequestBuilder FromJson(string json, Action? configure = null) + { + // 空检查 + ArgumentException.ThrowIfNullOrWhiteSpace(json); + + /* + * 手动解析 JSON 字符串 + * + * 不采用 JSON 反序列化的原因如下: + * 1. HttpRequestBuilder 的属性设计为只读,无法直接通过反序列化赋值。 + * 2. 避免引入 [JsonInclude] 特性对 System.Text.Json 的强耦合,保持依赖解耦。 + * 3. 简化 JSON 字符串的结构定义,无需严格遵循 HttpRequestBuilder 的属性定义,从而省略 [JsonPropertyName] 等自定义映射。 + * 4. 精确控制需要解析的键,减少不必要的自定义 JsonConverter 操作,提升性能与可维护性。 + */ + var jsonObject = JsonNode.Parse(json, new JsonNodeOptions { PropertyNameCaseInsensitive = true }, + new JsonDocumentOptions { AllowTrailingCommas = true })?.AsObject(); + + // 空检查 + ArgumentNullException.ThrowIfNull(jsonObject); + + // 验证必填字段 + if (!jsonObject.TryGetPropertyValue("method", out var methodNode) || methodNode is not JsonValue methodValue) + { + throw new ArgumentException("Missing required `method` in JSON."); + } + + // 允许 "url" 为 null,但必须定义 + if (!jsonObject.ContainsKey("url")) + { + throw new ArgumentException("Missing required `url` in JSON."); + } + + // 初始化 HttpRequestBuilder 实例 + var httpRequestBuilder = Create(methodValue.ToString(), jsonObject["url"]?.GetValue()); + + // 处理可选字段 + HandleJsonNode(jsonObject, "baseAddress", node => httpRequestBuilder.SetBaseAddress(node.GetValue())); + HandleJsonNode(jsonObject, "headers", node => httpRequestBuilder.WithHeaders(node)); + HandleJsonNode(jsonObject, "queries", node => httpRequestBuilder.WithQueryParameters(node)); + HandleJsonNode(jsonObject, "cookies", node => httpRequestBuilder.WithCookies(node)); + HandleJsonNode(jsonObject, "timeout", node => httpRequestBuilder.SetTimeout(node.GetValue())); + HandleJsonNode(jsonObject, "client", node => httpRequestBuilder.SetHttpClientName(node.GetValue())); + HandleJsonNode(jsonObject, "profiler", node => httpRequestBuilder.Profiler(node.GetValue())); + + // 处理请求内容 + if (jsonObject.TryGetPropertyValue("data", out var dataNode)) + { + // "data" 和 "contentType" 必须同时存在或同时不存在 + if (!jsonObject.TryGetPropertyValue("contentType", out var contentTypeNode) || + contentTypeNode is not JsonValue contentTypeValue) + { + throw new InvalidOperationException("The `contentType` key is required when `data` is present."); + } + + // 设置请求内容 + httpRequestBuilder + .SetContent( + dataNode?.ToJsonString(new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }), contentTypeValue.ToString()).AddStringContentForFormUrlEncodedContentProcessor(); + + // 设置内容编码 + HandleJsonNode(jsonObject, "encoding", + node => httpRequestBuilder.SetContentEncoding(node.GetValue())); + } + + // 处理多部分表单 + if (jsonObject.TryGetPropertyValue("multipart", out var multipartNode)) + { + // 设置多部分表单内容 + httpRequestBuilder.SetMultipartContent(multipart => multipart.AddJson(multipartNode?.AsObject() + .ToJsonString(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }))); + } + + // 调用自定义配置委托 + configure?.Invoke(httpRequestBuilder); + + return httpRequestBuilder; + } + + /// + /// 处理 + /// + /// + /// + /// + /// 属性名 + /// 自定义操作 + internal static void HandleJsonNode(JsonObject jsonObject, string propertyName, Action action) + { + // 空检查 + ArgumentNullException.ThrowIfNull(jsonObject); + ArgumentNullException.ThrowIfNull(propertyName); + ArgumentNullException.ThrowIfNull(action); + + if (jsonObject.TryGetPropertyValue(propertyName, out var node) && node is not null) + { + action(node); + } + } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs index e89ec2807..43137c58c 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs @@ -76,6 +76,12 @@ public sealed partial class HttpRequestBuilder // 初始化 HttpRequestMessage 实例 var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri); + // 设置 HTTP 版本 + if (Version is not null) + { + httpRequestMessage.Version = Version; + } + // 启用性能优化 EnablePerformanceOptimization(httpRequestMessage); @@ -160,18 +166,44 @@ public sealed partial class HttpRequestBuilder /// internal void AppendPathSegments(UriBuilder uriBuilder) { + // 空检查 + if ((PathSegments == null || PathSegments.Count == 0) && + (PathSegmentsToRemove == null || PathSegmentsToRemove.Count == 0)) + { + return; + } + + // 记录原路径是否以斜杠结尾(修复核心逻辑) + var originalPath = uriBuilder.Uri.AbsolutePath; + var endsWithSlash = originalPath.Length > 1 && originalPath.EndsWith('/'); + // 解析 URL 中的路径片段列表 - var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries).Concat([]); + var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries); - // 追加路径片段 - pathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([]).Where(u => !string.IsNullOrWhiteSpace(u)) - .Select(u => u.TrimStart('/').TrimEnd('/'))); + // 追加并处理新路径片段 + var newPathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([]) + .Where(u => !string.IsNullOrWhiteSpace(u)).Select(u => u.TrimStart('/').TrimEnd('/'))); - // 构建路径片段赋值给 UriBuilder 的 Path 属性 - uriBuilder.Path = '/' + string.Join('/', - // 过滤已标记为移除的路径片段 - pathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 }, - u => PathSegmentsToRemove?.TryGetValue(u, out _) == false)); + // 过滤需要移除的路径片段 + var filteredSegments = newPathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 }, + u => PathSegmentsToRemove?.Contains(u) == false).ToArray(); + + // 构建最终路径 + if (filteredSegments.Length != 0) + { + uriBuilder.Path = $"/{string.Join('/', filteredSegments)}"; + + // 恢复原路径的结尾斜杠(当存在路径片段时) + if (endsWithSlash) + { + uriBuilder.Path += "/"; + } + } + // 没有路径片段时设置为根路径 + else + { + uriBuilder.Path = "/"; + } } /// @@ -182,6 +214,13 @@ public sealed partial class HttpRequestBuilder /// internal void AppendQueryParameters(UriBuilder uriBuilder) { + // 空检查 + if ((QueryParameters is null || QueryParameters.Count == 0) && + (QueryParametersToRemove is null || QueryParametersToRemove.Count == 0)) + { + return; + } + // 解析 URL 中的查询字符串为键值对列表 var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?'); @@ -300,6 +339,16 @@ public sealed partial class HttpRequestBuilder // 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中 foreach (var (key, values) in Headers) { + // 替换 Referer 标头的 "{BASE_ADDRESS}" 模板字符串 + if (key.IsIn([HeaderNames.Referer], StringComparer.OrdinalIgnoreCase) && + values.FirstOrDefault() == Constants.REFERER_HEADER_BASE_ADDRESS_TEMPLATE) + { + httpRequestMessage.Headers.Referrer = new Uri( + $"{httpRequestMessage.RequestUri?.Scheme}://{httpRequestMessage.RequestUri?.Host}{(httpRequestMessage.RequestUri?.IsDefaultPort != true ? $":{httpRequestMessage.RequestUri?.Port}" : string.Empty)}", + UriKind.RelativeOrAbsolute); + continue; + } + httpRequestMessage.Headers.TryAddWithoutValidation(key, values); } } @@ -486,6 +535,18 @@ public sealed partial class HttpRequestBuilder // 构建 HttpContent 实例 var httpContent = httpContentProcessorFactory.Build(RawContent, ContentType!, ContentEncoding, processors); + // 空检查 + if (httpContent is null) + { + return; + } + + // 检查是否移除默认的内容的 Content-Type,解决对接 Java 程序时可能出现失败问题 + if (OmitContentType) + { + httpContent.Headers.ContentType = null; + } + // 调用用于处理在设置请求消息的内容时的操作 OnPreSetContent?.Invoke(httpContent); @@ -513,6 +574,9 @@ public sealed partial class HttpRequestBuilder { httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE"); } + + // 添加 HttpClient 实例的配置名称 + httpRequestMessage.Options.AddOrUpdate(Constants.HTTP_CLIENT_NAME, HttpClientName ?? string.Empty); } /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs index c0adc6def..30317cc35 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs @@ -88,15 +88,26 @@ internal static class Constants /// 被用于从 Options 属性中读取。 internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__"; + /// + /// HTTP 请求 实例的配置名称键 + /// + /// 被用于从 Options 属性中读取。 + internal const string HTTP_CLIENT_NAME = "__HTTP_CLIENT_NAME__"; + /// /// 浏览器的 User-Agent 标头值 /// internal const string USER_AGENT_OF_BROWSER = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"; + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0"; /// /// 移动端浏览器的 User-Agent 标头值 /// internal const string USER_AGENT_OF_MOBILE_BROWSER = - "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.36 Edg/133.0.0.0"; + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 Edg/135.0.0.0"; + + /// + /// Referer 标头请求基地址模板 + /// + internal const string REFERER_HEADER_BASE_ADDRESS_TEMPLATE = "{BASE_ADDRESS}"; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs index 3984238a5..0c1871d1d 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System.Net.Http.Json; +using System.Text.Json; namespace ThingsGateway.HttpRemote; @@ -27,16 +28,45 @@ public class ObjectContentConverter : IHttpContentConverter /// public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken = default) => - httpResponseMessage.Content.ReadFromJsonAsync(resultType, - ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ?? - HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult(); + httpResponseMessage.Content + .ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), cancellationToken) + .GetAwaiter().GetResult(); /// public virtual async Task ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken = default) => - await httpResponseMessage.Content.ReadFromJsonAsync(resultType, - ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ?? - HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false); + await httpResponseMessage.Content.ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), + cancellationToken).ConfigureAwait(false); + + /// + /// 获取 JSON 序列化选项实例 + /// + /// + /// + /// + /// + /// + /// + protected virtual JsonSerializerOptions GetJsonSerializerOptions(HttpResponseMessage httpResponseMessage) + { + // 空检查 + ArgumentNullException.ThrowIfNull(httpResponseMessage); + + // 获取 HttpClient 实例的配置名称 + if (httpResponseMessage.RequestMessage?.Options.TryGetValue( + new HttpRequestOptionsKey(Constants.HTTP_CLIENT_NAME), out var httpClientName) != true) + { + httpClientName = string.Empty; + } + + // 获取 HttpClientOptions 实例 + var httpClientOptions = ServiceProvider?.GetService>()?.Get(httpClientName); + + // 优先级:指定名称的 HttpClientOptions -> HttpRemoteOptions -> 默认值 + return (httpClientOptions?.IsDefault != false ? null : httpClientOptions.JsonSerializerOptions) ?? + ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ?? + HttpRemoteOptions.JsonSerializerOptionsDefault; + } } /// @@ -48,14 +78,13 @@ public class ObjectContentConverter : ObjectContentConverter, IHttpCont /// public virtual TResult? Read(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken = default) => - httpResponseMessage.Content.ReadFromJsonAsync( - ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ?? - HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult(); + httpResponseMessage.Content + .ReadFromJsonAsync(GetJsonSerializerOptions(httpResponseMessage), cancellationToken).GetAwaiter() + .GetResult(); /// public virtual async Task ReadAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken = default) => - await httpResponseMessage.Content.ReadFromJsonAsync( - ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ?? - HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false); + await httpResponseMessage.Content.ReadFromJsonAsync(GetJsonSerializerOptions(httpResponseMessage), + cancellationToken).ConfigureAwait(false); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/VoidContentConverter.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/VoidContentConverter.cs index b278d5c22..18c5ac92f 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/VoidContentConverter.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/VoidContentConverter.cs @@ -18,10 +18,10 @@ public class VoidContentConverter : HttpContentConverterBase { /// public override VoidContent? Read(HttpResponseMessage httpResponseMessage, - CancellationToken cancellationToken = default) => default; + CancellationToken cancellationToken = default) => null; /// public override Task ReadAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken = default) => - Task.FromResult(default); + Task.FromResult(null); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/HttpVersionAttribute.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/HttpVersionAttribute.cs new file mode 100644 index 000000000..2366f5740 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/HttpVersionAttribute.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.HttpRemote; + +/// +/// HTTP 声明式 HTTP 版本特性 +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] +public sealed class HttpVersionAttribute : Attribute +{ + /// + /// + /// + /// HTTP 版本 + public HttpVersionAttribute(string? version) => Version = version; + + /// + /// HTTP 版本 + /// + public string? Version { get; set; } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/RefererAttribute.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/RefererAttribute.cs new file mode 100644 index 000000000..2afa9ed8f --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/RefererAttribute.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.HttpRemote; + +/// +/// HTTP 声明式请求来源地址特性 +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] +public sealed class RefererAttribute : Attribute +{ + /// + /// + /// + /// 请求来源地址,当设置为 "{BASE_ADDRESS}" 时将替换为基地址 + public RefererAttribute(string? referer) => Referer = referer; + + /// + /// 请求来源地址,当设置为 "{BASE_ADDRESS}" 时将替换为基地址 + /// + public string? Referer { get; set; } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/SuppressExceptionsAttribute.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/SuppressExceptionsAttribute.cs new file mode 100644 index 000000000..c12a5897b --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/SuppressExceptionsAttribute.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.HttpRemote; + +/// +/// HTTP 声明式异常抑制特性 +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] +public sealed class SuppressExceptionsAttribute : Attribute +{ + /// + /// + /// + /// 抑制所有异常。 + public SuppressExceptionsAttribute() + : this(true) + { + } + + /// + /// + /// + /// 是否启用异常抑制。当设置为 false 时,将禁用异常抑制机制。 + public SuppressExceptionsAttribute(bool enabled) => Types = enabled ? [typeof(Exception)] : []; + + /// + /// + /// + /// 异常抑制类型集合 + public SuppressExceptionsAttribute(params Type[] types) => Types = types; + + /// + /// 异常抑制类型集合 + /// + public Type[] Types { get; set; } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Builders/HttpDeclarativeBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Builders/HttpDeclarativeBuilder.cs index 66bbdf3a7..5f83127b3 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Builders/HttpDeclarativeBuilder.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Builders/HttpDeclarativeBuilder.cs @@ -44,8 +44,11 @@ public sealed class HttpDeclarativeBuilder new(typeof(QueryDeclarativeExtractor), new QueryDeclarativeExtractor()), new(typeof(PathDeclarativeExtractor), new PathDeclarativeExtractor()), new(typeof(CookieDeclarativeExtractor), new CookieDeclarativeExtractor()), + new(typeof(RefererDeclarativeExtractor), new RefererDeclarativeExtractor()), new(typeof(HeaderDeclarativeExtractor), new HeaderDeclarativeExtractor()), new(typeof(PropertyDeclarativeExtractor), new PropertyDeclarativeExtractor()), + new(typeof(HttpVersionDeclarativeExtractor), new HttpVersionDeclarativeExtractor()), + new(typeof(SuppressExceptionsDeclarativeExtractor), new SuppressExceptionsDeclarativeExtractor()), new(typeof(BodyDeclarativeExtractor), new BodyDeclarativeExtractor()) ]); diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HeaderDeclarativeExtractor.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HeaderDeclarativeExtractor.cs index afcad33af..f53325f6f 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HeaderDeclarativeExtractor.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HeaderDeclarativeExtractor.cs @@ -45,7 +45,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor if (headerAttribute.HasSetValue) { httpRequestBuilder.WithHeader(headerName, headerAttribute.Value, headerAttribute.Escape, - replace: headerAttribute.Replace); + headerAttribute.Replace); } // 移除请求标头 else @@ -91,7 +91,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection()) { httpRequestBuilder.WithHeader(parameterName, value ?? headerAttribute.Value, - headerAttribute.Escape, replace: headerAttribute.Replace); + headerAttribute.Escape, headerAttribute.Replace); continue; } @@ -99,7 +99,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor // 空检查 if (value is not null) { - httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, replace: headerAttribute.Replace); + httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, headerAttribute.Replace); } } } diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs new file mode 100644 index 000000000..07038d28f --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.HttpRemote; + +/// +/// HTTP 声明式 特性提取器 +/// +internal sealed class HttpVersionDeclarativeExtractor : IHttpDeclarativeExtractor +{ + /// + public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context) + { + // 检查方法或接口是否贴有 [HttpVersion] 特性 + if (!context.IsMethodDefined(out var versionAttribute, true)) + { + return; + } + + // 设置 HTTP 版本 + httpRequestBuilder.SetVersion(versionAttribute.Version); + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/QueryDeclarativeExtractor.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/QueryDeclarativeExtractor.cs index a86842418..48d5e6627 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/QueryDeclarativeExtractor.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/QueryDeclarativeExtractor.cs @@ -45,7 +45,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor if (queryAttribute.HasSetValue) { httpRequestBuilder.WithQueryParameter(queryName, queryAttribute.Value, queryAttribute.Escape, - replace: queryAttribute.Replace, ignoreNullValues: queryAttribute.IgnoreNullValues); + queryAttribute.Replace, queryAttribute.IgnoreNullValues); } // 移除查询参数 else @@ -91,8 +91,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection()) { httpRequestBuilder.WithQueryParameter(parameterName, value ?? queryAttribute.Value, - queryAttribute.Escape, replace: queryAttribute.Replace, - ignoreNullValues: queryAttribute.IgnoreNullValues); + queryAttribute.Escape, queryAttribute.Replace, queryAttribute.IgnoreNullValues); continue; } @@ -101,7 +100,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor if (value is not null) { httpRequestBuilder.WithQueryParameters(value, queryAttribute.Prefix, queryAttribute.Escape, - replace: queryAttribute.Replace, ignoreNullValues: queryAttribute.IgnoreNullValues); + queryAttribute.Replace, queryAttribute.IgnoreNullValues); } } } diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/RefererDeclarativeExtractor.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/RefererDeclarativeExtractor.cs new file mode 100644 index 000000000..42ccf3489 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/RefererDeclarativeExtractor.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.HttpRemote; + +/// +/// HTTP 声明式 特性提取器 +/// +internal sealed class RefererDeclarativeExtractor : IHttpDeclarativeExtractor +{ + /// + public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context) + { + // 检查方法或接口是否贴有 [Referer] 特性 + if (!context.IsMethodDefined(out var refererAttribute, true)) + { + return; + } + + // 设置请求来源地址 + httpRequestBuilder.SetReferer(refererAttribute.Referer); + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs new file mode 100644 index 000000000..d5cea3b54 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.HttpRemote; + +/// +/// HTTP 声明式 特性提取器 +/// +internal sealed class SuppressExceptionsDeclarativeExtractor : IHttpDeclarativeExtractor +{ + /// + public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context) + { + // 检查方法或接口是否贴有 [SuppressExceptions] 特性 + if (!context.IsMethodDefined(out var suppressExceptionsAttribute, true)) + { + return; + } + + // 设置异常抑制 + httpRequestBuilder.SuppressExceptions(suppressExceptionsAttribute.Types); + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/HttpDeclarativeExtractorContext.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/HttpDeclarativeExtractorContext.cs index 72bc226a4..f61dd94b8 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/HttpDeclarativeExtractorContext.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/HttpDeclarativeExtractorContext.cs @@ -25,7 +25,7 @@ public sealed class HttpDeclarativeExtractorContext /// 冻结参数类型集合 /// /// 此类参数类型不应作为外部提取对象。 - internal static Type[] _frozenParameterTypes = + internal static readonly Type[] _frozenParameterTypes = [ typeof(Action), typeof(Action), typeof(HttpCompletionOption), typeof(CancellationToken) diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpContextExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpContextExtensions.cs index 8c708a0da..b76ffd20f 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpContextExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpContextExtensions.cs @@ -42,7 +42,7 @@ public static partial class HttpContextExtensions /// /// /// - internal static HashSet _ignoreResponseHeaders = + internal static readonly HashSet _ignoreResponseHeaders = [ "Content-Type", "Connection", "Transfer-Encoding", "Keep-Alive", "Upgrade", "Proxy-Connection" ]; @@ -64,7 +64,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpResponseMessage Forward(this HttpContext? httpContext, string? requestUri = null, + public static HttpResponseMessage? Forward(this HttpContext? httpContext, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -90,7 +90,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpResponseMessage Forward(this HttpContext? httpContext, HttpMethod httpMethod, + public static HttpResponseMessage? Forward(this HttpContext? httpContext, HttpMethod httpMethod, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -115,7 +115,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpResponseMessage Forward(this HttpContext? httpContext, Uri? requestUri = null, + public static HttpResponseMessage? Forward(this HttpContext? httpContext, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -140,7 +140,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpResponseMessage Forward(this HttpContext? httpContext, HttpMethod httpMethod, + public static HttpResponseMessage? Forward(this HttpContext? httpContext, HttpMethod httpMethod, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) @@ -180,7 +180,7 @@ public static partial class HttpContextExtensions /// /// /// - public static Task ForwardAsync(this HttpContext? httpContext, string? requestUri = null, + public static Task ForwardAsync(this HttpContext? httpContext, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -206,7 +206,7 @@ public static partial class HttpContextExtensions /// /// /// - public static Task ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, + public static Task ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -231,7 +231,7 @@ public static partial class HttpContextExtensions /// /// /// - public static Task ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null, + public static Task ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -256,7 +256,7 @@ public static partial class HttpContextExtensions /// /// /// - public static async Task ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, + public static async Task ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) @@ -297,7 +297,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpRemoteResult Forward(this HttpContext? httpContext, string? requestUri = null, + public static HttpRemoteResult? Forward(this HttpContext? httpContext, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -324,7 +324,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpRemoteResult Forward(this HttpContext? httpContext, HttpMethod httpMethod, + public static HttpRemoteResult? Forward(this HttpContext? httpContext, HttpMethod httpMethod, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -350,7 +350,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpRemoteResult Forward(this HttpContext? httpContext, Uri? requestUri = null, + public static HttpRemoteResult? Forward(this HttpContext? httpContext, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -376,7 +376,7 @@ public static partial class HttpContextExtensions /// /// /// - public static HttpRemoteResult Forward(this HttpContext? httpContext, HttpMethod httpMethod, + public static HttpRemoteResult? Forward(this HttpContext? httpContext, HttpMethod httpMethod, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) @@ -393,7 +393,7 @@ public static partial class HttpContextExtensions var result = httpRemoteService.Send(httpRequestBuilder, completionOption, httpContext.RequestAborted); // 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中 - ForwardResponseMessage(httpContext, result.ResponseMessage, httpContextForwardBuilder.ForwardOptions); + ForwardResponseMessage(httpContext, result?.ResponseMessage, httpContextForwardBuilder.ForwardOptions); return result; } @@ -416,7 +416,7 @@ public static partial class HttpContextExtensions /// /// /// - public static Task> ForwardAsync(this HttpContext? httpContext, + public static Task?> ForwardAsync(this HttpContext? httpContext, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -443,7 +443,7 @@ public static partial class HttpContextExtensions /// /// /// - public static Task> ForwardAsync(this HttpContext? httpContext, + public static Task?> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, string? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -469,7 +469,7 @@ public static partial class HttpContextExtensions /// /// /// - public static Task> ForwardAsync(this HttpContext? httpContext, + public static Task?> ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) => @@ -495,7 +495,7 @@ public static partial class HttpContextExtensions /// /// /// - public static async Task> ForwardAsync(this HttpContext? httpContext, + public static async Task?> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, Uri? requestUri = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpContextForwardOptions? forwardOptions = null) @@ -513,7 +513,7 @@ public static partial class HttpContextExtensions httpContext.RequestAborted).ConfigureAwait(false); // 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中 - ForwardResponseMessage(httpContext, result.ResponseMessage, httpContextForwardBuilder.ForwardOptions); + ForwardResponseMessage(httpContext, result?.ResponseMessage, httpContextForwardBuilder.ForwardOptions); return result; } @@ -605,14 +605,22 @@ public static partial class HttpContextExtensions /// /// /// - internal static void ForwardResponseMessage(HttpContext httpContext, HttpResponseMessage httpResponseMessage, + internal static void ForwardResponseMessage(HttpContext httpContext, HttpResponseMessage? httpResponseMessage, HttpContextForwardOptions forwardOptions) { // 空检查 ArgumentNullException.ThrowIfNull(httpContext); - ArgumentNullException.ThrowIfNull(httpResponseMessage); ArgumentNullException.ThrowIfNull(forwardOptions); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + return; + } + // 获取 HttpResponse 实例 var httpResponse = httpContext.Response; diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpMultipartFormDataBuilderExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpMultipartFormDataBuilderExtensions.cs new file mode 100644 index 000000000..0d0e95dfb --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpMultipartFormDataBuilderExtensions.cs @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Http; + +using System.Text; + +namespace ThingsGateway.HttpRemote; + +/// +/// 拓展类 +/// +public static class HttpMultipartFormDataBuilderExtensions +{ + /// + /// 添加文件 + /// + /// + /// + /// + /// + /// + /// + /// 表单名称 + /// 文件的名称 + /// 内容类型 + /// 内容编码 + /// + /// + /// + public static HttpMultipartFormDataBuilder AddFile(this HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, + IFormFile formFile, string? name = null, string? fileName = null, string? contentType = null, + Encoding? contentEncoding = null) + { + // 空检查 + ArgumentNullException.ThrowIfNull(formFile); + + // 初始化 MemoryStream 实例 + var memoryStream = new MemoryStream(); + + // 将 IFormFile 内容复制到内存流 + formFile.CopyTo(memoryStream); + + // 将内存流的位置重置到起始位置 + memoryStream.Position = 0; + + // 添加文件流 + return httpMultipartFormDataBuilder.AddStream(memoryStream, name ?? formFile.Name, + fileName ?? formFile.FileName, contentType ?? formFile.ContentType, contentEncoding, + true); + } + + /// + /// 添加多个文件 + /// + /// + /// + /// + /// + /// + /// + /// 表单名称 + /// + /// + /// + public static HttpMultipartFormDataBuilder AddFiles(this HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, + IEnumerable formFiles, string? name = null) + { + // 空检查 + ArgumentNullException.ThrowIfNull(formFiles); + + // 逐条添加文件 + foreach (var formFile in formFiles) + { + httpMultipartFormDataBuilder.AddFile(formFile, name ?? formFile.Name); + } + + return httpMultipartFormDataBuilder; + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpRemoteExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpRemoteExtensions.cs index 4c866a8b0..2c52d980c 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpRemoteExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpRemoteExtensions.cs @@ -16,6 +16,7 @@ using Microsoft.Net.Http.Headers; using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; +using System.Text.RegularExpressions; using ThingsGateway.Extensions; using ThingsGateway.Utilities; @@ -27,7 +28,7 @@ namespace ThingsGateway.HttpRemote.Extensions; /// /// HTTP 远程服务拓展类 /// -public static class HttpRemoteExtensions +public static partial class HttpRemoteExtensions { /// /// 添加 HTTP 远程请求分析工具处理委托 @@ -70,6 +71,56 @@ public static class HttpRemoteExtensions builder.AddProfilerDelegatingHandler(() => disableInProduction && GetHostEnvironmentName(builder.Services)?.ToLower() == "production"); + /// + /// 配置 额外选项 + /// + /// + /// + /// + /// 自定义配置选项 + /// + /// + /// + public static IHttpClientBuilder ConfigureOptions(this IHttpClientBuilder builder, + Action configure) + { + // 空检查 + ArgumentNullException.ThrowIfNull(configure); + + builder.Services.AddOptions(builder.Name).Configure(options => + { + options.IsDefault = false; + configure.Invoke(options); + }); + + return builder; + } + + /// + /// 配置 额外选项 + /// + /// + /// + /// + /// 自定义配置选项 + /// + /// + /// + public static IHttpClientBuilder ConfigureOptions(this IHttpClientBuilder builder, + Action configure) + { + // 空检查 + ArgumentNullException.ThrowIfNull(configure); + + builder.Services.AddOptions(builder.Name).Configure((options, provider) => + { + options.IsDefault = false; + configure.Invoke(options, provider); + }); + + return builder; + } + /// /// 为 启用性能优化 /// @@ -160,17 +211,26 @@ public static class HttpRemoteExtensions ? [new KeyValuePair>("Declarative", [methodSignature])] : null; + // 格式化 HttpClient 实例的配置条目 + IEnumerable>>? httpClientKeyValues = + httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey(Constants.HTTP_CLIENT_NAME), + out var httpClientName) + ? [new KeyValuePair>("HttpClient Name", [httpClientName])] + : null; + // 格式化常规条目 var generalEntry = StringUtility.FormatKeyValuesSummary(new[] - { - new KeyValuePair>("Request URL", - [httpRequestMessage.RequestUri?.OriginalString!]), - new KeyValuePair>("HTTP Method", [httpRequestMessage.Method.ToString()]), - new KeyValuePair>("Status Code", - [$"{(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}"]), - new KeyValuePair>("HTTP Content", - [$"{httpContent?.GetType().Name}"]) - }.ConcatIgnoreNull(declarativeKeyValues).ConcatIgnoreNull(generalCustomKeyValues), generalSummary); + { + new KeyValuePair>("Request URL", + [httpRequestMessage.RequestUri?.OriginalString!]), + new KeyValuePair>("HTTP Method", [httpRequestMessage.Method.ToString()]), + new KeyValuePair>("Status Code", + [$"{(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}"]), + new KeyValuePair>("HTTP Version", [httpResponseMessage.Version.ToString()]), + new KeyValuePair>("HTTP Content", + [$"{httpContent?.GetType().Name}"]) + }.ConcatIgnoreNull(httpClientKeyValues).ConcatIgnoreNull(declarativeKeyValues) + .ConcatIgnoreNull(generalCustomKeyValues), generalSummary); // 格式化响应条目 var responseEntry = httpResponseMessage.ProfilerHeaders(responseSummary); @@ -203,19 +263,43 @@ public static class HttpRemoteExtensions // 默认只读取 5KB 的内容 const int maxBytesToDisplay = 5120; - // 读取内容为字节数组 + /* + * 读取内容为字节数组 + * + * 由于 HttpContent 的流设计为单次读取(即流内容在首次读取后会被消耗,无法重复读取), + * 当前实现(即使用 ReadAsByteArrayAsync(cancellationToken))中对于较大内容会一次性加载至内存, + * 这可能导致性能问题(如内存占用过高或响应延迟),不过目前尚未找到更优的解决方案。 + * + * 强烈建议在生产环境中禁用或关闭此类一次性读取操作,尤其是对于高并发或大流量场景, + * 以避免因内存溢出(OOM)或线程阻塞导致的服务不可用风险。 + */ var buffer = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + var total = buffer.Length; // 计算要显示的部分 - var bytesToShow = Math.Min(buffer.Length, maxBytesToDisplay); - var partialContent = Encoding.UTF8.GetString(buffer, 0, bytesToShow); + var bytesToShow = Math.Min(total, maxBytesToDisplay); + + // 注册 CodePagesEncodingProvider,使得程序能够识别并使用 Windows 代码页中的各种编码 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // 获取内容编码 + var charset = httpContent.Headers.ContentType?.CharSet ?? "utf-8"; + var partialContent = Encoding.GetEncoding(charset).GetString(buffer, 0, bytesToShow); + + // 检查是否是完整的 Unicode 转义字符串 + if (total == bytesToShow && UnicodeEscapeRegex().IsMatch(partialContent)) + { + partialContent = Regex.Unescape(partialContent); + } // 如果实际读取的数据小于最大显示大小,则直接返回;否则,添加省略号表示内容被截断 - var bodyString = buffer.Length <= maxBytesToDisplay ? partialContent : partialContent + " ... [truncated]"; + var bodyString = total <= maxBytesToDisplay + ? partialContent + : partialContent + $" ... [truncated, total: {total} bytes]"; return StringUtility.FormatKeyValuesSummary( [new KeyValuePair>(string.Empty, [bodyString])], - $"{summary} ({httpContent.GetType().Name})"); + $"{summary} ({httpContent.GetType().Name}, total: {total} bytes)"); } /// @@ -359,4 +443,13 @@ public static class HttpRemoteExtensions ? null : Convert.ToString(hostEnvironment.GetType().GetProperty("EnvironmentName")?.GetValue(hostEnvironment)); } + + /// + /// Unicode 转义正则表达式 + /// + /// + /// + /// + [GeneratedRegex(@"\\u([0-9a-fA-F]{4})")] + private static partial Regex UnicodeEscapeRegex(); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/HttpContentConverterFactory.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/HttpContentConverterFactory.cs index 32eb8f24d..eb5e11dd2 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/HttpContentConverterFactory.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/HttpContentConverterFactory.cs @@ -52,27 +52,44 @@ internal sealed class HttpContentConverterFactory : IHttpContentConverterFactory public IServiceProvider ServiceProvider { get; } /// - public TResult? Read(HttpResponseMessage httpResponseMessage, IHttpContentConverter[]? converters = null, + public TResult? Read(HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default) => - GetConverter(converters).Read(httpResponseMessage, cancellationToken); + httpResponseMessage is null + ? default + : GetConverter(converters).Read(httpResponseMessage, cancellationToken); /// - public object? Read(Type resultType, HttpResponseMessage httpResponseMessage, - IHttpContentConverter[]? converters = null, - CancellationToken cancellationToken = default) => - GetConverter(resultType, converters).Read(resultType, httpResponseMessage, cancellationToken); + public object? Read(Type resultType, HttpResponseMessage? httpResponseMessage, + IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default) => + httpResponseMessage is null + ? null + : GetConverter(resultType, converters).Read(resultType, httpResponseMessage, cancellationToken); /// - public async Task ReadAsync(HttpResponseMessage httpResponseMessage, - IHttpContentConverter[]? converters = null, - CancellationToken cancellationToken = default) => - await GetConverter(converters).ReadAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); + public async Task ReadAsync(HttpResponseMessage? httpResponseMessage, + IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default) + { + // 空检查 + if (httpResponseMessage is null) + { + return default; + } + + return await GetConverter(converters).ReadAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); + } /// - public async Task ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, - IHttpContentConverter[]? converters = null, - CancellationToken cancellationToken = default) => - await GetConverter(resultType, converters).ReadAsync(resultType, httpResponseMessage, cancellationToken).ConfigureAwait(false); + public async Task ReadAsync(Type resultType, HttpResponseMessage? httpResponseMessage, + IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default) + { + // 空检查 + if (httpResponseMessage is null) + { + return null; + } + + return await GetConverter(resultType, converters).ReadAsync(resultType, httpResponseMessage, cancellationToken).ConfigureAwait(false); + } /// /// 获取 实例 diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/IHttpContentConverterFactory.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/IHttpContentConverterFactory.cs index e302df669..8cc4b8acb 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/IHttpContentConverterFactory.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Factories/IHttpContentConverterFactory.cs @@ -37,7 +37,7 @@ public interface IHttpContentConverterFactory /// /// /// - TResult? Read(HttpResponseMessage httpResponseMessage, + TResult? Read(HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); /// @@ -54,7 +54,7 @@ public interface IHttpContentConverterFactory /// /// /// - object? Read(Type resultType, HttpResponseMessage httpResponseMessage, + object? Read(Type resultType, HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); /// @@ -73,7 +73,7 @@ public interface IHttpContentConverterFactory /// /// /// - Task ReadAsync(HttpResponseMessage httpResponseMessage, + Task ReadAsync(HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); /// @@ -90,6 +90,6 @@ public interface IHttpContentConverterFactory /// /// /// - Task ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, + Task ReadAsync(Type resultType, HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileDownloadManager.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileDownloadManager.cs index 0bd4833fd..2087f1edf 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileDownloadManager.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileDownloadManager.cs @@ -115,6 +115,15 @@ internal sealed class FileDownloadManager var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + return; + } + // 根据文件是否存在及配置的行为来决定是否应继续进行文件下载 if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath)) { @@ -228,6 +237,15 @@ internal sealed class FileDownloadManager var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + return; + } + // 根据文件是否存在及配置的行为来决定是否应继续进行文件下载 if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath)) { @@ -246,7 +264,7 @@ internal sealed class FileDownloadManager bufferSize, true); // 获取 HTTP 响应体中的内容流 - using var contentStream = (await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); + using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); // 循环读取数据直到取消请求或读取完毕 int numBytesRead; @@ -485,6 +503,9 @@ internal sealed class FileDownloadManager /// internal string GetFileName(HttpResponseMessage httpResponseMessage) { + // 空检查 + ArgumentNullException.ThrowIfNull(httpResponseMessage); + // 获取文件下载保存的文件的名称 var fileName = Path.GetFileName(_httpFileDownloadBuilder.DestinationPath); diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileUploadManager.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileUploadManager.cs index 54d1527b4..b25a7c71b 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileUploadManager.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/FileUploadManager.cs @@ -88,7 +88,7 @@ internal sealed class FileUploadManager /// /// /// - internal HttpResponseMessage Start(CancellationToken cancellationToken = default) + internal HttpResponseMessage? Start(CancellationToken cancellationToken = default) { // 创建进度报告任务取消标识 using var progressCancellationTokenSource = new CancellationTokenSource(); @@ -102,7 +102,7 @@ internal sealed class FileUploadManager // 初始化 Stopwatch 实例并开启计时操作 var stopwatch = Stopwatch.StartNew(); - HttpResponseMessage httpResponseMessage; + HttpResponseMessage? httpResponseMessage; try { @@ -147,7 +147,7 @@ internal sealed class FileUploadManager /// /// /// - internal async Task StartAsync(CancellationToken cancellationToken = default) + internal async Task StartAsync(CancellationToken cancellationToken = default) { // 创建进度报告任务取消标识 using var progressCancellationTokenSource = new CancellationTokenSource(); @@ -161,7 +161,7 @@ internal sealed class FileUploadManager // 初始化 Stopwatch 实例并开启计时操作 var stopwatch = Stopwatch.StartNew(); - HttpResponseMessage httpResponseMessage; + HttpResponseMessage? httpResponseMessage; try { diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/LongPollingManager.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/LongPollingManager.cs index e7cb30388..2f8aa9893 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/LongPollingManager.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/LongPollingManager.cs @@ -97,6 +97,15 @@ internal sealed class LongPollingManager // 发送 HTTP 远程请求 var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, cancellationToken); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + continue; + } + // 发送响应数据对象到通道 dataChannel.Writer.TryWrite(httpResponseMessage); @@ -167,6 +176,15 @@ internal sealed class LongPollingManager // 发送 HTTP 远程请求 var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, cancellationToken).ConfigureAwait(false); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + continue; + } + // 发送响应数据对象到通道 await dataChannel.Writer.WriteAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/ServerSentEventsManager.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/ServerSentEventsManager.cs index f7714e2b3..119721dcf 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/ServerSentEventsManager.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/ServerSentEventsManager.cs @@ -109,6 +109,15 @@ internal sealed class ServerSentEventsManager var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + return; + } + // 获取 HTTP 响应体中的内容流 using var contentStream = httpResponseMessage.Content.ReadAsStream(cancellationToken); @@ -203,9 +212,17 @@ internal sealed class ServerSentEventsManager var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - // 获取 HTTP 响应体中的内容流 - using var contentStream = (await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + return; + } + + // 获取 HTTP 响应体中的内容流 + using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); // 初始化 StreamReader 实例 using var streamReader = new StreamReader(contentStream, Encoding.UTF8); @@ -382,10 +399,10 @@ internal sealed class ServerSentEventsManager ? retryInterval : _httpServerSentEventsBuilder.DefaultRetryInterval; break; - // 所有其他的字段名都会被忽略 + // 其他的字段名存储在 CustomFields 属性中 default: - // 保持数据不变 - return true; + serverSentEventsData.AddCustomField(key, value); + break; } return true; diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/StressTestHarnessManager.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/StressTestHarnessManager.cs index 5fd367dae..52a31bbde 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/StressTestHarnessManager.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Managers/StressTestHarnessManager.cs @@ -143,6 +143,15 @@ internal sealed class StressTestHarnessManager var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, completionOption, cancellationToken).ConfigureAwait(false); + // 空检查 + if (httpResponseMessage is null) + { + // 输出调试信息 + Debugging.Error("The response content was not read, as it was empty."); + + return; + } + // 检查响应状态码是否是成功状态 if (httpResponseMessage.IsSuccessStatusCode) { diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteAnalyzer.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteAnalyzer.cs index 51a8db934..8dd980911 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteAnalyzer.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteAnalyzer.cs @@ -36,7 +36,7 @@ public sealed class HttpRemoteAnalyzer /// /// 分析数据 /// - public string Data => _cachedData ??= _dataBuffer.ToString(); + public string Data => _cachedData ??= _dataBuffer.ToString().TrimEnd(Environment.NewLine.ToCharArray()); /// /// 追加分析数据 @@ -44,7 +44,7 @@ public sealed class HttpRemoteAnalyzer /// 分析数据 internal void AppendData(string? value) { - _dataBuffer.Append(value); + _dataBuffer.AppendLine(value); _cachedData = null; } diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteClient.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteClient.cs new file mode 100644 index 000000000..76cb1f06c --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteClient.cs @@ -0,0 +1,186 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; + +namespace ThingsGateway.HttpRemote; + +/// +/// 提供静态访问 服务的方式 +/// +/// 支持服务的延迟初始化、配置更新以及资源释放。 +#pragma warning disable CA1513 +public static class HttpRemoteClient +{ + /// + internal static IServiceProvider? _serviceProvider; + + /// + /// 延迟加载的 实例 + /// + internal static Lazy _lazyService; + + /// + /// 并发锁对象 + /// + internal static readonly object _lock = new(); + + /// + /// 标记服务是否已释放 + /// + internal static bool _isDisposed; + + /// + /// 自定义服务注册逻辑的委托 + /// + internal static Action _configure = services => services.AddHttpRemote(); + + /// + /// + /// + static HttpRemoteClient() => _lazyService = new Lazy(CreateService); + + /// + /// 获取当前配置下的 实例 + /// + /// + public static IHttpRemoteService Service + { + get + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(HttpRemoteClient)); + } + + return _lazyService.Value; + } + } + + /// + /// 自定义服务注册逻辑 + /// + public static void Configure(Action configure) + { + // 空检查 + ArgumentNullException.ThrowIfNull(configure); + + lock (_lock) + { + _configure = configure; + + // 重新初始化服务 + Reinitialize(); + } + } + + /// + /// 释放服务提供器及相关资源 + /// + /// 通常在应用程序关闭或不再需要 HTTP 远程请求服务时调用。 + public static void Dispose() + { + lock (_lock) + { + if (_isDisposed) + { + return; + } + + // 释放服务提供器 + ReleaseServiceProvider(); + + _isDisposed = true; + } + } + + /// + /// 创建 实例 + /// + /// + /// + /// + /// + /// + internal static IHttpRemoteService CreateService() + { + lock (_lock) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(HttpRemoteClient)); + } + + // 如果值已创建,直接返回 + if (_lazyService.IsValueCreated) + { + return _lazyService.Value; + } + + try + { + // 初始化 ServiceCollection 实例 + var services = new ServiceCollection(); + + // 调用自定义服务注册逻辑的委托 + _configure(services); + + // 构建服务提供器 + _serviceProvider = services.BuildServiceProvider(); + + // 解析 IHttpRemoteService 实例 + var service = _serviceProvider.GetRequiredService(); + + return service; + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to initialize IHttpRemoteService.", ex); + } + } + } + + /// + /// 使用最新的 配置重新初始化服务 + /// + /// + internal static void Reinitialize() + { + lock (_lock) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(HttpRemoteClient)); + } + + // 释放当前的服务提供器 + ReleaseServiceProvider(); + + // 重新创建延迟加载实例 + _lazyService = new Lazy(CreateService, LazyThreadSafetyMode.ExecutionAndPublication); + } + } + + /// + /// 释放服务提供器 + /// + internal static void ReleaseServiceProvider() + { + // 如果服务提供器支持释放资源,则执行释放操作 + if (_serviceProvider is IDisposable disposable) + { + disposable.Dispose(); + } + + _serviceProvider = null; + } +} +#pragma warning restore CA1513 \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteResult.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteResult.cs index dc05465dd..5763476e8 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteResult.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteResult.cs @@ -108,6 +108,80 @@ public sealed class HttpRemoteResult /// public HttpContentHeaders ContentHeaders { get; private set; } = null!; + /// + /// HTTP 版本 + /// + public Version Version { get; private set; } = null!; + + /// + /// 实例的配置名称 + /// + public string? HttpClientName { get; private set; } + + // /// + // /// 解构函数(至少包含两个 out 参数!!!) + // /// + // /// + // /// + // /// + // public void Deconstruct(out TResult? result) + // { + // result = Result; + // } + + /// + /// 解构函数 + /// + /// + /// + /// + /// + /// + /// + public void Deconstruct(out TResult? result, out HttpResponseMessage httpResponseMessage) + { + result = Result; + httpResponseMessage = ResponseMessage; + } + + /// + /// 解构函数 + /// + /// + /// + /// + /// + /// + /// + /// 是否请求成功 + public void Deconstruct(out TResult? result, out HttpResponseMessage httpResponseMessage, + out bool isSuccessStatusCode) + { + result = Result; + httpResponseMessage = ResponseMessage; + isSuccessStatusCode = IsSuccessStatusCode; + } + + /// + /// 解构函数 + /// + /// + /// + /// + /// + /// + /// + /// 是否请求成功 + /// 响应状态码 + public void Deconstruct(out TResult? result, out HttpResponseMessage httpResponseMessage, + out bool isSuccessStatusCode, out HttpStatusCode statusCode) + { + result = Result; + httpResponseMessage = ResponseMessage; + isSuccessStatusCode = IsSuccessStatusCode; + statusCode = StatusCode; + } + /// /// 初始化 /// @@ -124,6 +198,16 @@ public sealed class HttpRemoteResult // 解析响应标头 Set-Cookie 集合 ParseSetCookies(ResponseMessage.Headers); + + // 获取 HTTP 版本 + Version = ResponseMessage.Version; + + // 获取 HttpClient 实例的配置名称 + if (ResponseMessage.RequestMessage?.Options.TryGetValue( + new HttpRequestOptionsKey(Constants.HTTP_CLIENT_NAME), out var httpClientName) == true) + { + HttpClientName = httpClientName; + } } /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/ServerSentEventsData.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/ServerSentEventsData.cs index fb87af0ca..4717c0dd9 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/ServerSentEventsData.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/ServerSentEventsData.cs @@ -9,6 +9,7 @@ // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // ------------------------------------------------------------------------ +using System.Collections.ObjectModel; using System.Text; namespace ThingsGateway.HttpRemote; @@ -19,6 +20,11 @@ namespace ThingsGateway.HttpRemote; /// 参考文献:https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events#%E5%AD%97%E6%AE%B5。 public sealed class ServerSentEventsData { + /// + /// 用于存储自定义的字段数据 + /// + internal readonly List> _customFields; + /// /// 消息数据构建器 /// @@ -32,7 +38,11 @@ public sealed class ServerSentEventsData /// /// /// - internal ServerSentEventsData() => _dataBuffer = new StringBuilder(); + internal ServerSentEventsData() + { + _dataBuffer = new StringBuilder(); + _customFields = []; + } /// /// 事件类型 @@ -61,6 +71,12 @@ public sealed class ServerSentEventsData /// 重新连接的时间。如果与服务器的连接丢失,浏览器将等待指定的时间,然后尝试重新连接。这必须是一个整数,以毫秒为单位指定重新连接的时间。如果指定了一个非整数值,该字段将被忽略。 public int Retry { get; internal set; } + /// + /// 自定义的字段数据 + /// + public IReadOnlyCollection> CustomFields => + new ReadOnlyCollection>(_customFields); + /// /// 追加消息数据 /// @@ -70,4 +86,12 @@ public sealed class ServerSentEventsData _dataBuffer.Append(value); _cachedData = null; } + + /// + /// 追加自定义字段数据 + /// + /// 字段名 + /// 字段数据 + internal void AddCustomField(string name, string value) => + _customFields.Add(new KeyValuePair(name, value)); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpClientOptions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpClientOptions.cs new file mode 100644 index 000000000..ddcadcdfe --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpClientOptions.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.Options; + +using System.Text.Json; + +namespace ThingsGateway.HttpRemote; + +/// +/// 配置选项 +/// +public sealed class HttpClientOptions +{ + /// + /// JSON 序列化配置 + /// + public JsonSerializerOptions JsonSerializerOptions { get; set; } = + new(HttpRemoteOptions.JsonSerializerOptionsDefault); + + /// + /// 标识选项是否配置为默认值(未配置) + /// + /// 用于避免通过 获取选项时无法确定是否已配置该选项。默认值为:true + internal bool IsDefault { get; set; } = true; +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpRemoteOptions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpRemoteOptions.cs index 565f3d9b2..8fdd82a8c 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpRemoteOptions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpRemoteOptions.cs @@ -12,9 +12,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using ThingsGateway.Converters.Json; + namespace ThingsGateway.HttpRemote; /// @@ -30,7 +33,18 @@ public sealed class HttpRemoteOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - NumberHandling = JsonNumberHandling.AllowReadingFromString + // 允许 String 转 Number + NumberHandling = JsonNumberHandling.AllowReadingFromString, + // 解决中文乱码问题 + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + AllowTrailingCommas = true, + Converters = + { + new DateTimeConverterUsingDateTimeParseAsFallback(), + new DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback(), + // 允许 Number 或 Boolean 转 String + new StringJsonConverter() + } }; /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Processors/StringContentProcessor.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Processors/StringContentProcessor.cs index 4bc44d311..2edf4cdea 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Processors/StringContentProcessor.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Processors/StringContentProcessor.cs @@ -18,6 +18,7 @@ using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using ThingsGateway.Extensions; @@ -52,7 +53,7 @@ public class StringContentProcessor : HttpContentProcessorBase } // 将原始请求内容转换为字符串 - var content = rawContent.GetType().IsBasicType() || rawContent is JsonElement + var content = rawContent.GetType().IsBasicType() || rawContent is JsonElement or JsonNode ? rawContent.ToCultureString(CultureInfo.InvariantCulture) : JsonSerializer.Serialize(rawContent, ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ?? diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.Extensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.Extensions.cs index b1da2416b..2449d1577 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.Extensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.Extensions.cs @@ -51,7 +51,7 @@ internal sealed partial class HttpRemoteService new FileDownloadManager(this, httpFileDownloadBuilder, configure).StartAsync(cancellationToken); /// - public HttpResponseMessage UploadFile(string? requestUri, string filePath, string name = "file", + public HttpResponseMessage? UploadFile(string? requestUri, string filePath, string name = "file", Func? onProgressChanged = null, string? fileName = null, Action? configure = null, Action? requestConfigure = null, CancellationToken cancellationToken = default) => @@ -60,7 +60,7 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public Task UploadFileAsync(string? requestUri, string filePath, string name = "file", + public Task UploadFileAsync(string? requestUri, string filePath, string name = "file", Func? onProgressChanged = null, string? fileName = null, Action? configure = null, Action? requestConfigure = null, CancellationToken cancellationToken = default) => @@ -69,12 +69,12 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpResponseMessage Send(HttpFileUploadBuilder httpFileUploadBuilder, + public HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder, Action? configure = null, CancellationToken cancellationToken = default) => new FileUploadManager(this, httpFileUploadBuilder, configure).Start(cancellationToken); /// - public Task SendAsync(HttpFileUploadBuilder httpFileUploadBuilder, + public Task SendAsync(HttpFileUploadBuilder httpFileUploadBuilder, Action? configure = null, CancellationToken cancellationToken = default) => new FileUploadManager(this, httpFileUploadBuilder, configure).StartAsync(cancellationToken); diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.HttpMethods.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.HttpMethods.cs index 9bc03fd78..2f67a92da 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.HttpMethods.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.HttpMethods.cs @@ -17,22 +17,22 @@ namespace ThingsGateway.HttpRemote; internal sealed partial class HttpRemoteService { /// - public HttpResponseMessage Get(string? requestUri, Action? configure = null, + public HttpResponseMessage? Get(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Get(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Get(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Get(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); /// - public Task GetAsync(string? requestUri, Action? configure = null, + public Task GetAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task GetAsync(string? requestUri, HttpCompletionOption completionOption, + public Task GetAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); @@ -58,22 +58,22 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Get(string? requestUri, Action? configure = null, + public HttpRemoteResult? Get(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Get(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Get(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Get(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); /// - public Task> GetAsync(string? requestUri, + public Task?> GetAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> GetAsync(string? requestUri, HttpCompletionOption completionOption, + public Task?> GetAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); @@ -133,22 +133,22 @@ internal sealed partial class HttpRemoteService GetAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Put(string? requestUri, Action? configure = null, + public HttpResponseMessage? Put(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Put(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Put(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Put(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); /// - public Task PutAsync(string? requestUri, Action? configure = null, + public Task PutAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => PutAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task PutAsync(string? requestUri, HttpCompletionOption completionOption, + public Task PutAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); @@ -174,22 +174,22 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Put(string? requestUri, Action? configure = null, + public HttpRemoteResult? Put(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Put(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Put(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Put(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); /// - public Task> PutAsync(string? requestUri, + public Task?> PutAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => PutAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> PutAsync(string? requestUri, HttpCompletionOption completionOption, + public Task?> PutAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); @@ -249,23 +249,23 @@ internal sealed partial class HttpRemoteService PutAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Post(string? requestUri, Action? configure = null, + public HttpResponseMessage? Post(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Post(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Post(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Post(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); /// - public Task PostAsync(string? requestUri, Action? configure = null, + public Task PostAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => PostAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task PostAsync(string? requestUri, HttpCompletionOption completionOption, + public Task PostAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); @@ -291,22 +291,23 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Post(string? requestUri, Action? configure = null, + public HttpRemoteResult? Post(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Post(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Post(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Post(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); /// - public Task> PostAsync(string? requestUri, + public Task?> PostAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => PostAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> PostAsync(string? requestUri, HttpCompletionOption completionOption, + public Task?> PostAsync(string? requestUri, + HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); @@ -366,23 +367,23 @@ internal sealed partial class HttpRemoteService PostAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Delete(string? requestUri, Action? configure = null, + public HttpResponseMessage? Delete(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Delete(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Delete(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Delete(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken); /// - public Task DeleteAsync(string? requestUri, Action? configure = null, + public Task DeleteAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => DeleteAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task DeleteAsync(string? requestUri, HttpCompletionOption completionOption, + public Task DeleteAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken); @@ -408,22 +409,22 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Delete(string? requestUri, Action? configure = null, + public HttpRemoteResult? Delete(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Delete(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Delete(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Delete(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken); /// - public Task> DeleteAsync(string? requestUri, + public Task?> DeleteAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => DeleteAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> DeleteAsync(string? requestUri, + public Task?> DeleteAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, @@ -487,23 +488,23 @@ internal sealed partial class HttpRemoteService DeleteAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Head(string? requestUri, Action? configure = null, + public HttpResponseMessage? Head(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Head(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Head(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Head(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); /// - public Task HeadAsync(string? requestUri, Action? configure = null, + public Task HeadAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => HeadAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task HeadAsync(string? requestUri, HttpCompletionOption completionOption, + public Task HeadAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); @@ -529,22 +530,23 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Head(string? requestUri, Action? configure = null, + public HttpRemoteResult? Head(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Head(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Head(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Head(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); /// - public Task> HeadAsync(string? requestUri, + public Task?> HeadAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => HeadAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> HeadAsync(string? requestUri, HttpCompletionOption completionOption, + public Task?> HeadAsync(string? requestUri, + HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); @@ -604,23 +606,23 @@ internal sealed partial class HttpRemoteService HeadAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Options(string? requestUri, Action? configure = null, + public HttpResponseMessage? Options(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Options(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Options(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Options(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken); /// - public Task OptionsAsync(string? requestUri, Action? configure = null, + public Task OptionsAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => OptionsAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task OptionsAsync(string? requestUri, HttpCompletionOption completionOption, + public Task OptionsAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken); @@ -646,22 +648,22 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Options(string? requestUri, Action? configure = null, + public HttpRemoteResult? Options(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Options(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Options(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Options(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken); /// - public Task> OptionsAsync(string? requestUri, + public Task?> OptionsAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => OptionsAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> OptionsAsync(string? requestUri, + public Task?> OptionsAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, @@ -725,23 +727,23 @@ internal sealed partial class HttpRemoteService OptionsAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Trace(string? requestUri, Action? configure = null, + public HttpResponseMessage? Trace(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Trace(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Trace(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Trace(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken); /// - public Task TraceAsync(string? requestUri, Action? configure = null, + public Task TraceAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => TraceAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task TraceAsync(string? requestUri, HttpCompletionOption completionOption, + public Task TraceAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken); @@ -767,22 +769,22 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Trace(string? requestUri, Action? configure = null, + public HttpRemoteResult? Trace(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Trace(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Trace(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Trace(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken); /// - public Task> TraceAsync(string? requestUri, + public Task?> TraceAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => TraceAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> TraceAsync(string? requestUri, + public Task?> TraceAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, @@ -846,23 +848,23 @@ internal sealed partial class HttpRemoteService TraceAsAsync(requestUri, completionOption, configure, cancellationToken); /// - public HttpResponseMessage Patch(string? requestUri, Action? configure = null, + public HttpResponseMessage? Patch(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Patch(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpResponseMessage Patch(string? requestUri, HttpCompletionOption completionOption, + public HttpResponseMessage? Patch(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken); /// - public Task PatchAsync(string? requestUri, Action? configure = null, + public Task PatchAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => PatchAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task PatchAsync(string? requestUri, HttpCompletionOption completionOption, + public Task PatchAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync( HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken); @@ -888,22 +890,22 @@ internal sealed partial class HttpRemoteService cancellationToken); /// - public HttpRemoteResult Patch(string? requestUri, Action? configure = null, + public HttpRemoteResult? Patch(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => Patch(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public HttpRemoteResult Patch(string? requestUri, HttpCompletionOption completionOption, + public HttpRemoteResult? Patch(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => Send( HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken); /// - public Task> PatchAsync(string? requestUri, + public Task?> PatchAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default) => PatchAsync(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); /// - public Task> PatchAsync(string? requestUri, + public Task?> PatchAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default) => SendAsync(HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.cs index c84564e1e..d805c4f0f 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/HttpRemoteService.cs @@ -90,16 +90,16 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService public IServiceProvider ServiceProvider { get; } /// - public HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, + public HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) => Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); /// - public HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, + public HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default) { // 发送 HTTP 远程请求 - var (httpResponseMessage, _) = SendCoreAsync(httpRequestBuilder, completionOption, default, + var (httpResponseMessage, _) = SendCoreAsync(httpRequestBuilder, completionOption, null, (httpClient, httpRequestMessage, option, token) => httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); @@ -107,18 +107,18 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService } /// - public Task SendAsync(HttpRequestBuilder httpRequestBuilder, + public Task SendAsync(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) => SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); /// - public async Task SendAsync(HttpRequestBuilder httpRequestBuilder, + public async Task SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default) { // 发送 HTTP 远程请求 var (httpResponseMessage, _) = await SendCoreAsync(httpRequestBuilder, completionOption, (httpClient, httpRequestMessage, option, token) => - httpClient.SendAsync(httpRequestMessage, option, token), default, cancellationToken).ConfigureAwait(false); + httpClient.SendAsync(httpRequestMessage, option, token), null, cancellationToken).ConfigureAwait(false); return httpResponseMessage; } @@ -133,7 +133,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService CancellationToken cancellationToken = default) { // 发送 HTTP 远程请求 - var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, default, + var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, null, (httpClient, httpRequestMessage, option, token) => httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); @@ -156,7 +156,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService cancellationToken); // 动态创建 HttpRemoteResult 实例并转换为 TResult 实例 - return (TResult)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration); + return (TResult?)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration); } /// @@ -199,7 +199,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService // 发送 HTTP 远程请求 var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption, (httpClient, httpRequestMessage, option, token) => - httpClient.SendAsync(httpRequestMessage, option, token), default, cancellationToken).ConfigureAwait(false); + httpClient.SendAsync(httpRequestMessage, option, token), null, cancellationToken).ConfigureAwait(false); // 获取结果类型 var resultType = typeof(TResult); @@ -220,7 +220,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService cancellationToken).ConfigureAwait(false); // 动态创建 HttpRemoteResult 实例并转换为 TResult 实例 - return (TResult)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration); + return (TResult?)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration); } /// @@ -263,7 +263,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService CancellationToken cancellationToken = default) { // 发送 HTTP 远程请求 - var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, default, + var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, null, (httpClient, httpRequestMessage, option, token) => httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); @@ -298,7 +298,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService // 发送 HTTP 远程请求 var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption, (httpClient, httpRequestMessage, option, token) => - httpClient.SendAsync(httpRequestMessage, option, token), default, cancellationToken).ConfigureAwait(false); + httpClient.SendAsync(httpRequestMessage, option, token), null, cancellationToken).ConfigureAwait(false); // 检查类型是否是 HttpRemoteResult 类型 if (!typeof(HttpRemoteResult<>).IsDefinitionEqual(resultType)) @@ -320,19 +320,25 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService } /// - public HttpRemoteResult Send(HttpRequestBuilder httpRequestBuilder, + public HttpRemoteResult? Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) => Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); /// - public HttpRemoteResult Send(HttpRequestBuilder httpRequestBuilder, + public HttpRemoteResult? Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default) { // 发送 HTTP 远程请求 - var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, default, + var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, null, (httpClient, httpRequestMessage, option, token) => httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); + // 空检查 + if (httpResponseMessage is null) + { + return null; + } + // 将 HttpResponseMessage 转换为 TResult 实例 var result = _httpContentConverterFactory.Read(httpResponseMessage, httpRequestBuilder.HttpContentConverterProviders?.SelectMany(u => u.Invoke()).ToArray(), cancellationToken); @@ -348,18 +354,24 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService } /// - public Task> SendAsync(HttpRequestBuilder httpRequestBuilder, + public Task?> SendAsync(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) => SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); /// - public async Task> SendAsync(HttpRequestBuilder httpRequestBuilder, + public async Task?> SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default) { // 发送 HTTP 远程请求 var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption, (httpClient, httpRequestMessage, option, token) => - httpClient.SendAsync(httpRequestMessage, option, token), default, cancellationToken).ConfigureAwait(false); + httpClient.SendAsync(httpRequestMessage, option, token), null, cancellationToken).ConfigureAwait(false); + + // 空检查 + if (httpResponseMessage is null) + { + return null; + } // 将 HttpResponseMessage 转换为 TResult 实例 var result = await _httpContentConverterFactory.ReadAsync(httpResponseMessage, @@ -392,7 +404,8 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService /// /// /// - internal async Task<(HttpResponseMessage ResponseMessage, long RequestDuration)> SendCoreAsync( + /// + internal async Task<(HttpResponseMessage? ResponseMessage, long RequestDuration)> SendCoreAsync( HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, Func>? sendAsyncMethod, @@ -445,6 +458,20 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService // 设置单次请求超时时间控制 if (httpRequestBuilder.Timeout is not null && httpRequestBuilder.Timeout.Value != TimeSpan.Zero) { + // 确保 HttpRequestBuilder 的 Timeout 属性值小于 HttpClient 的 Timeout 属性值(默认 100秒) + if (httpRequestBuilder.Timeout.Value > httpClient.Timeout) + { + throw new InvalidOperationException( + "HttpRequestBuilder's Timeout cannot be greater than HttpClient's Timeout, which defaults to 100 seconds."); + } + + // 调用超时发生时要执行的操作 + if (httpRequestBuilder.TimeoutAction is not null) + { + timeoutCancellationTokenSource.Token.Register(httpRequestBuilder.TimeoutAction.TryInvoke); + } + + // 延迟指定时间后取消任务 timeoutCancellationTokenSource.CancelAfter(httpRequestBuilder.Timeout.Value); } @@ -534,7 +561,13 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService // 处理发送 HTTP 请求发生异常 HandleRequestFailed(httpRequestBuilder, requestEventHandler, e, httpResponseMessage); - throw; + // 检查是否启用异常抑制机制 + if (!ShouldSuppressException(httpRequestBuilder.SuppressExceptionTypes, e)) + { + throw; + } + + return (httpResponseMessage, requestDuration); } finally { @@ -706,7 +739,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService ArgumentNullException.ThrowIfNull(httpClient); // 添加默认的 User-Agent 标头 - AddDefaultUserAgentHeader(httpClient); + AddDefaultUserAgentHeader(httpClient, httpRequestBuilder); // 存储 HttpClientPooling 实例并返回 return httpRequestBuilder.HttpClientPooling = new HttpClientPooling(httpClient, release); @@ -719,10 +752,15 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService /// /// /// - internal static void AddDefaultUserAgentHeader(HttpClient httpClient) + /// + /// + /// + internal static void AddDefaultUserAgentHeader(HttpClient httpClient, HttpRequestBuilder httpRequestBuilder) { // 空检查 - if (httpClient.DefaultRequestHeaders.UserAgent.Count != 0) + if (httpClient.DefaultRequestHeaders.UserAgent.Count != 0 || + httpRequestBuilder.HeadersToRemove?.Contains(HeaderNames.UserAgent) == true || + httpRequestBuilder.Headers?.ContainsKey(HeaderNames.UserAgent) == true) { return; } @@ -854,10 +892,10 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService int.TryParse(stringStatusCode, out var intStatusCodeResult) && intStatusCodeResult == statusCode: return true; - // 处理字符串区间类型,如 200-500 + // 处理字符串区间类型,如 200-500 或 200~500 case string stringStatusCode when StatusCodeRangeRegex().IsMatch(stringStatusCode): - // 根据 - 符号切割 - var parts = stringStatusCode.Split('-', StringSplitOptions.RemoveEmptyEntries); + // 根据 - 或 ~ 符号切割 + var parts = stringStatusCode.Split(['-', '~'], StringSplitOptions.RemoveEmptyEntries); // 比较状态码区间 if (parts.Length == 2 && int.TryParse(parts[0], out var start) && int.TryParse(parts[1], out var end)) @@ -888,8 +926,6 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService "=" => statusCode == number, _ => false }; - default: - return false; } return false; @@ -908,9 +944,8 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService /// /// /// - internal static object DynamicCreateHttpRemoteResult(Type httpRemoteResultType, - HttpResponseMessage httpResponseMessage, - object? result, long requestDuration) + internal static object? DynamicCreateHttpRemoteResult(Type httpRemoteResultType, + HttpResponseMessage? httpResponseMessage, object? result, long requestDuration) { // 检查类型是否是 HttpRemoteResult 类型 if (!typeof(HttpRemoteResult<>).IsDefinitionEqual(httpRemoteResultType)) @@ -920,6 +955,12 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService nameof(httpRemoteResultType)); } + // 空检查 + if (httpResponseMessage is null) + { + return null; + } + // 反射创建 HttpRemoteResult 实例 var httpRemoteResult = Activator.CreateInstance(httpRemoteResultType, httpResponseMessage); @@ -946,11 +987,32 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService return httpRemoteResult; } + /// + /// 检查是否启用异常抑制机制 + /// + /// 受抑制的异常类型列表 + /// + /// + /// + /// + /// + /// + internal static bool ShouldSuppressException(HashSet? suppressExceptionTypes, Exception? exception) + { + // 空检查 + if (suppressExceptionTypes is null or { Count: 0 } || exception is null) + { + return false; + } + + return suppressExceptionTypes.Any(u => u.IsInstanceOfType(exception)); + } + /// /// 状态码区间正则表达式 /// /// - [GeneratedRegex(@"^\d+-\d+$")] + [GeneratedRegex(@"^\d+[-~]\d+$")] private static partial Regex StatusCodeRangeRegex(); /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.Extensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.Extensions.cs index 916137a7b..7662cc6b7 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.Extensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.Extensions.cs @@ -106,7 +106,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage UploadFile(string? requestUri, string filePath, string name = "file", + HttpResponseMessage? UploadFile(string? requestUri, string filePath, string name = "file", Func? onProgressChanged = null, string? fileName = null, Action? configure = null, Action? requestConfigure = null, CancellationToken cancellationToken = default); @@ -127,7 +127,7 @@ public partial interface IHttpRemoteService /// /// /// - Task UploadFileAsync(string? requestUri, string filePath, string name = "file", + Task UploadFileAsync(string? requestUri, string filePath, string name = "file", Func? onProgressChanged = null, string? fileName = null, Action? configure = null, Action? requestConfigure = null, CancellationToken cancellationToken = default); @@ -145,7 +145,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Send(HttpFileUploadBuilder httpFileUploadBuilder, Action? configure = null, + HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -161,7 +161,7 @@ public partial interface IHttpRemoteService /// /// /// - Task SendAsync(HttpFileUploadBuilder httpFileUploadBuilder, + Task SendAsync(HttpFileUploadBuilder httpFileUploadBuilder, Action? configure = null, CancellationToken cancellationToken = default); /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.HttpMethods.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.HttpMethods.cs index a5c507b83..e0156e967 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.HttpMethods.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.HttpMethods.cs @@ -27,7 +27,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Get(string? requestUri, Action? configure = null, + HttpResponseMessage? Get(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -44,7 +44,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Get(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Get(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -58,7 +58,7 @@ public partial interface IHttpRemoteService /// /// /// - Task GetAsync(string? requestUri, Action? configure = null, + Task GetAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -75,7 +75,7 @@ public partial interface IHttpRemoteService /// /// /// - Task GetAsync(string? requestUri, HttpCompletionOption completionOption, + Task GetAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -156,7 +156,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Get(string? requestUri, Action? configure = null, + HttpRemoteResult? Get(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -174,7 +174,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Get(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Get(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -189,7 +189,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> GetAsync(string? requestUri, Action? configure = null, + Task?> GetAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -207,7 +207,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> GetAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> GetAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -407,7 +407,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Put(string? requestUri, Action? configure = null, + HttpResponseMessage? Put(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -424,7 +424,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Put(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Put(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -438,7 +438,7 @@ public partial interface IHttpRemoteService /// /// /// - Task PutAsync(string? requestUri, Action? configure = null, + Task PutAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -455,7 +455,7 @@ public partial interface IHttpRemoteService /// /// /// - Task PutAsync(string? requestUri, HttpCompletionOption completionOption, + Task PutAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -536,7 +536,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Put(string? requestUri, Action? configure = null, + HttpRemoteResult? Put(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -554,7 +554,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Put(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Put(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -569,7 +569,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> PutAsync(string? requestUri, Action? configure = null, + Task?> PutAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -587,7 +587,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> PutAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> PutAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -787,7 +787,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Post(string? requestUri, Action? configure = null, + HttpResponseMessage? Post(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -804,7 +804,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Post(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Post(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -818,7 +818,7 @@ public partial interface IHttpRemoteService /// /// /// - Task PostAsync(string? requestUri, Action? configure = null, + Task PostAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -835,7 +835,7 @@ public partial interface IHttpRemoteService /// /// /// - Task PostAsync(string? requestUri, HttpCompletionOption completionOption, + Task PostAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -916,7 +916,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Post(string? requestUri, Action? configure = null, + HttpRemoteResult? Post(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -934,7 +934,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Post(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Post(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -949,7 +949,8 @@ public partial interface IHttpRemoteService /// /// /// - Task> PostAsync(string? requestUri, Action? configure = null, + Task?> PostAsync(string? requestUri, + Action? configure = null, CancellationToken cancellationToken = default); /// @@ -967,7 +968,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> PostAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> PostAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1167,7 +1168,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Delete(string? requestUri, Action? configure = null, + HttpResponseMessage? Delete(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1184,7 +1185,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Delete(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Delete(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1198,7 +1199,7 @@ public partial interface IHttpRemoteService /// /// /// - Task DeleteAsync(string? requestUri, Action? configure = null, + Task DeleteAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1215,7 +1216,7 @@ public partial interface IHttpRemoteService /// /// /// - Task DeleteAsync(string? requestUri, HttpCompletionOption completionOption, + Task DeleteAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1296,7 +1297,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Delete(string? requestUri, Action? configure = null, + HttpRemoteResult? Delete(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1314,7 +1315,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Delete(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Delete(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1329,7 +1330,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> DeleteAsync(string? requestUri, + Task?> DeleteAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); @@ -1348,7 +1349,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> DeleteAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> DeleteAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1548,7 +1549,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Head(string? requestUri, Action? configure = null, + HttpResponseMessage? Head(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1565,7 +1566,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Head(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Head(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1579,7 +1580,7 @@ public partial interface IHttpRemoteService /// /// /// - Task HeadAsync(string? requestUri, Action? configure = null, + Task HeadAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1596,7 +1597,7 @@ public partial interface IHttpRemoteService /// /// /// - Task HeadAsync(string? requestUri, HttpCompletionOption completionOption, + Task HeadAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1677,7 +1678,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Head(string? requestUri, Action? configure = null, + HttpRemoteResult? Head(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1695,7 +1696,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Head(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Head(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1710,7 +1711,8 @@ public partial interface IHttpRemoteService /// /// /// - Task> HeadAsync(string? requestUri, Action? configure = null, + Task?> HeadAsync(string? requestUri, + Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1728,7 +1730,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> HeadAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> HeadAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1928,7 +1930,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Options(string? requestUri, Action? configure = null, + HttpResponseMessage? Options(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1945,7 +1947,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Options(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Options(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1959,7 +1961,7 @@ public partial interface IHttpRemoteService /// /// /// - Task OptionsAsync(string? requestUri, Action? configure = null, + Task OptionsAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -1976,7 +1978,7 @@ public partial interface IHttpRemoteService /// /// /// - Task OptionsAsync(string? requestUri, HttpCompletionOption completionOption, + Task OptionsAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2057,7 +2059,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Options(string? requestUri, Action? configure = null, + HttpRemoteResult? Options(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2075,7 +2077,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Options(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Options(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2090,7 +2092,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> OptionsAsync(string? requestUri, + Task?> OptionsAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); @@ -2109,7 +2111,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> OptionsAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> OptionsAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2309,7 +2311,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Trace(string? requestUri, Action? configure = null, + HttpResponseMessage? Trace(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2326,7 +2328,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Trace(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Trace(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2340,7 +2342,7 @@ public partial interface IHttpRemoteService /// /// /// - Task TraceAsync(string? requestUri, Action? configure = null, + Task TraceAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2357,7 +2359,7 @@ public partial interface IHttpRemoteService /// /// /// - Task TraceAsync(string? requestUri, HttpCompletionOption completionOption, + Task TraceAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2438,7 +2440,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Trace(string? requestUri, Action? configure = null, + HttpRemoteResult? Trace(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2456,7 +2458,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Trace(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Trace(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2471,7 +2473,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> TraceAsync(string? requestUri, + Task?> TraceAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); @@ -2490,7 +2492,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> TraceAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> TraceAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2690,7 +2692,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Patch(string? requestUri, Action? configure = null, + HttpResponseMessage? Patch(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2707,7 +2709,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Patch(string? requestUri, HttpCompletionOption completionOption, + HttpResponseMessage? Patch(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2721,7 +2723,7 @@ public partial interface IHttpRemoteService /// /// /// - Task PatchAsync(string? requestUri, Action? configure = null, + Task PatchAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2738,7 +2740,7 @@ public partial interface IHttpRemoteService /// /// /// - Task PatchAsync(string? requestUri, HttpCompletionOption completionOption, + Task PatchAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2819,7 +2821,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Patch(string? requestUri, Action? configure = null, + HttpRemoteResult? Patch(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2837,7 +2839,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Patch(string? requestUri, HttpCompletionOption completionOption, + HttpRemoteResult? Patch(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// @@ -2852,7 +2854,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> PatchAsync(string? requestUri, + Task?> PatchAsync(string? requestUri, Action? configure = null, CancellationToken cancellationToken = default); @@ -2871,7 +2873,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> PatchAsync(string? requestUri, HttpCompletionOption completionOption, + Task?> PatchAsync(string? requestUri, HttpCompletionOption completionOption, Action? configure = null, CancellationToken cancellationToken = default); /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.cs index 45ae5bf43..1ed5ebf50 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Services/IHttpRemoteService.cs @@ -33,7 +33,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default); + HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default); /// /// 发送 HTTP 远程请求 @@ -50,7 +50,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, + HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default); /// @@ -65,7 +65,7 @@ public partial interface IHttpRemoteService /// /// /// - Task SendAsync(HttpRequestBuilder httpRequestBuilder, + Task SendAsync(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default); /// @@ -83,7 +83,7 @@ public partial interface IHttpRemoteService /// /// /// - Task SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, + Task SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default); /// @@ -433,7 +433,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Send(HttpRequestBuilder httpRequestBuilder, + HttpRemoteResult? Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default); /// @@ -452,7 +452,7 @@ public partial interface IHttpRemoteService /// /// /// - HttpRemoteResult Send(HttpRequestBuilder httpRequestBuilder, + HttpRemoteResult? Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default); /// @@ -468,7 +468,7 @@ public partial interface IHttpRemoteService /// /// /// - Task> SendAsync(HttpRequestBuilder httpRequestBuilder, + Task?> SendAsync(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default); /// @@ -487,6 +487,6 @@ public partial interface IHttpRemoteService /// /// /// - Task> SendAsync(HttpRequestBuilder httpRequestBuilder, + Task?> SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Utilities/HttpRemoteUtility.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Utilities/HttpRemoteUtility.cs index 76ffe6ce1..32bcd4943 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Utilities/HttpRemoteUtility.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Utilities/HttpRemoteUtility.cs @@ -41,7 +41,7 @@ public static class HttpRemoteUtility /// 忽略 SSL 证书验证 /// public static Func IgnoreSslErrors => - (message, cert, chain, errors) => true; + (_, _, _, _) => true; /// /// 获取使用 IPv4 连接到服务器的回调 @@ -118,8 +118,20 @@ public static class HttpRemoteUtility // - IPv4: AddressFamily.InterNetwork // - IPv6: AddressFamily.InterNetworkV6 // - IPv4 或 IPv6: AddressFamily.Unspecified - // 注意:当主机没有 IP 地址时,此方法会抛出一个 SocketException 异常 - var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, addressFamily, cancellationToken).ConfigureAwait(false); + + IPAddress[] addresses; + + // 当主机是一个 IP 地址,无需进一步解析 + if (IPAddress.TryParse(context.DnsEndPoint.Host, out var ipAddress)) + { + addresses = [ipAddress]; + } + else + { + // 注意:当主机没有 IP 地址时,此方法会抛出一个 SocketException 异常 + var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, addressFamily, cancellationToken).ConfigureAwait(false); + addresses = entry.AddressList; + } // 打开与目标主机/端口的连接 var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); @@ -129,7 +141,7 @@ public static class HttpRemoteUtility try { - await socket.ConnectAsync(entry.AddressList, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false); + await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false); // 如果你想选择特定的 IP 地址来连接服务器 // await socket.ConnectAsync( diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketBinaryReceiveResult.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketBinaryReceiveResult.cs index 918ded2b9..6f4668952 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketBinaryReceiveResult.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketBinaryReceiveResult.cs @@ -34,5 +34,5 @@ public sealed class WebSocketBinaryReceiveResult : WebSocketReceiveResult /// /// 二进制消息 /// - public byte[] Message { get; internal init; } = default!; + public byte[] Message { get; internal init; } = null!; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketTextReceiveResult.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketTextReceiveResult.cs index d0d116b87..5eb714835 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketTextReceiveResult.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/WebSocket/WebSocketTextReceiveResult.cs @@ -34,5 +34,5 @@ public sealed class WebSocketTextReceiveResult : WebSocketReceiveResult /// /// 文本消息 /// - public string Message { get; internal init; } = default!; + public string Message { get; internal init; } = null!; } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/FallbackPolicy.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/FallbackPolicy.cs index 0a6cb2550..d7cbb4f1b 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/FallbackPolicy.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/FallbackPolicy.cs @@ -329,6 +329,7 @@ public class FallbackPolicy : PolicyBase { // 获取操作方法执行结果 context.Result = await operation(cancellationToken).ConfigureAwait(false); + context.Exception = null; } catch (System.Exception exception) { diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/RetryPolicy.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/RetryPolicy.cs index 3a1227eae..4d7a7e847 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/RetryPolicy.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/RescuePolicy/Policies/RetryPolicy.cs @@ -391,6 +391,7 @@ public class RetryPolicy : PolicyBase { // 获取操作方法执行结果 context.Result = await operation(cancellationToken).ConfigureAwait(false); + context.Exception = null; } catch (System.Exception exception) { diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Attributes/ClayAttribute.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Attributes/ClayAttribute.cs new file mode 100644 index 000000000..5b4794a62 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Attributes/ClayAttribute.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.Shapeless; + +/// +/// 流变对象模型绑定特性 +/// +/// 示例代码:[Clay] dynamic input +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ClayAttribute : Attribute; \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinder.cs index 227cf6434..b5019c6cc 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinder.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinder.cs @@ -14,7 +14,12 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using System.Net.Http.Headers; +using System.Net.Mime; using System.Reflection; +using System.Web; + +using ThingsGateway.Extensions; namespace ThingsGateway.Shapeless; @@ -35,9 +40,13 @@ internal sealed class ClayBinder(IOptions options) : IModelBinder // 获取 HttpContext 实例 var httpContext = bindingContext.HttpContext; + // 检查是否是 URL 表单(application/x-www-form-urlencoded)内容 + var isFormUrlEncoded = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType!).MediaType == + MediaTypeNames.Application.FormUrlEncoded; + // 尝试从请求体中读取数据,并将其转换为 Clay 实例 - var (canParse, model) = - await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options.Value, httpContext.RequestAborted).ConfigureAwait(false); + var (canParse, model) = await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options.Value, + isFormUrlEncoded, httpContext.RequestAborted).ConfigureAwait(false); bindingContext.Result = !canParse ? ModelBindingResult.Failed() : ModelBindingResult.Success(model); } @@ -49,6 +58,7 @@ internal sealed class ClayBinder(IOptions options) : IModelBinder /// /// /// + /// 是否是 application/x-www-form-urlencoded 表单 /// /// /// @@ -56,7 +66,7 @@ internal sealed class ClayBinder(IOptions options) : IModelBinder /// /// internal static async Task<(bool canParse, Clay? model)> TryReadAndConvertBodyToClayAsync(Stream stream, - ClayOptions options, CancellationToken cancellationToken) + ClayOptions options, bool isFormUrlEncoded, CancellationToken cancellationToken) { // 空检查 ArgumentNullException.ThrowIfNull(stream); @@ -65,7 +75,11 @@ internal sealed class ClayBinder(IOptions options) : IModelBinder using var streamReader = new StreamReader(stream); var json = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return string.IsNullOrEmpty(json) ? (false, null) : (true, Clay.Parse(json, options)); + return string.IsNullOrEmpty(json) + ? (false, null) + : (true, + Clay.Parse(isFormUrlEncoded ? HttpUtility.UrlDecode(json).ParseFormatKeyValueString(['&'], '?') : json, + options)); } /// @@ -89,9 +103,13 @@ internal sealed class ClayBinder(IOptions options) : IModelBinder // 解析 ClayOptions 选项 var options = httpContext.RequestServices.GetRequiredService>().Value; + // 检查是否是 URL 表单(application/x-www-form-urlencoded)内容 + var isFormUrlEncoded = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType!).MediaType == + MediaTypeNames.Application.FormUrlEncoded; + // 尝试从请求体流中读取数据,并将其转换为 Clay 实例 - var (_, model) = - await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options, httpContext.RequestAborted).ConfigureAwait(false); + var (_, model) = await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options, isFormUrlEncoded, + httpContext.RequestAborted).ConfigureAwait(false); return model; } diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinderProvider.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinderProvider.cs index 15473a0f6..58de1174e 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinderProvider.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Binders/ClayBinderProvider.cs @@ -11,6 +11,9 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; + +using System.Runtime.CompilerServices; namespace ThingsGateway.Shapeless; @@ -25,6 +28,15 @@ internal sealed class ClayBinderProvider : IModelBinderProvider // 空检查 ArgumentNullException.ThrowIfNull(context); - return context.Metadata.ModelType == typeof(Clay) ? new BinderTypeModelBinder(typeof(ClayBinder)) : null; + // 获取模型类型和参数特性列表 + var modelType = context.Metadata.ModelType; + var parameterAttributes = (context.Metadata as DefaultModelMetadata)?.Attributes.ParameterAttributes; + + return modelType == typeof(Clay) || + // 确保参数类型为 dynamic 且贴有 [Clay] 特性 + (modelType == typeof(object) && parameterAttributes?.OfType().Any() == true && + parameterAttributes.OfType().Any()) + ? new BinderTypeModelBinder(typeof(ClayBinder)) + : null; } } \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Enumerable.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Enumerable.cs index 1777da156..e96541240 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Enumerable.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Enumerable.cs @@ -37,6 +37,11 @@ public partial class Clay /// public IEnumerable Keys => AsEnumerable().Select(u => u.Key); + /// + /// 获取单一对象键(属性名)的列表 + /// + public IEnumerable MemberNames => AsEnumerateObject().Select(u => u.Key); + /// /// 获取值或元素的列表 /// @@ -127,6 +132,17 @@ public partial class Clay } } + /// + /// 将流变对象转换为 + /// + /// + /// + /// + public Dictionary ToDictionary() => + IsObject + ? As>()! + : As>()!.ToDictionary(u => u.Key.ToString(), u => u.Value); + /// /// 遍历 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Exports.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Exports.cs index 892a2db8a..90d5dc38c 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Exports.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Exports.cs @@ -10,6 +10,7 @@ // ------------------------------------------------------------------------ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -86,6 +87,14 @@ public partial class Clay /// public Clay this[Range range] => (Clay)this[range as object]!; + /// + /// 路径索引 + /// + /// 根据路径获取值。 + /// 带路径的标识符 + /// 是否是带路径的标识符 + public object? this[string identifier, bool isPath] => isPath ? PathValue(identifier) : GetValue(identifier); + /// /// 判断是否为单一对象 /// @@ -155,6 +164,35 @@ public partial class Clay return ToJsonString(jsonSerializerOptions); } + /// + /// 解构函数 + /// + /// dynamic 类型的 + /// + /// + /// + public void Deconstruct(out dynamic clay, out IEnumerable enumerableClay) + { + clay = this; + enumerableClay = this; + } + + /// + /// + /// dynamic 类型的 + /// + /// + /// + /// + /// + /// + public void Deconstruct(out dynamic clay, out IEnumerable enumerableClay, out Clay rawClay) + { + clay = this; + enumerableClay = this; + rawClay = this; + } + /// /// 创建空的单一对象 /// @@ -260,6 +298,38 @@ public partial class Clay public static Clay Parse(ref Utf8JsonReader utf8JsonReader, Action configure) => Parse(ref utf8JsonReader, ClayOptions.Default.Configure(configure)); + /// + /// 从文件中读取数据并转换为 实例 + /// + /// 文件路径 + /// + /// + /// + /// + /// + /// + public static Clay ParseFromFile(string path, ClayOptions? options = null) + { + // 空检查 + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + // 打开文件并读取流 + using var fileStream = File.OpenRead(path); + + return Parse(fileStream, options); + } + + /// + /// 从文件中读取数据并转换为 实例 + /// + /// 文件路径 + /// 自定义配置委托 + /// + /// + /// + public static Clay ParseFromFile(string path, Action configure) => + ParseFromFile(path, ClayOptions.Default.Configure(configure)); + /// /// 检查标识符是否定义 /// @@ -318,6 +388,37 @@ public partial class Clay /// public bool IsDefined(object identifier) => Contains(identifier); + /// + /// 检查属性(键)是否定义 + /// + /// 属性名(键) + /// + /// + /// + public bool HasProperty(string propertyName) + { + // 检查是否是集合或数组实例调用 + ThrowIfMethodCalledOnArrayCollection(nameof(HasProperty)); + + return Contains(propertyName); + } + + /// + /// 获取集合或数组中指定项(元素)的索引 + /// + /// 项(元素) + /// + /// + /// + public int IndexOf(object? value) + { + // 检查是否是单一对象实例调用 + ThrowIfMethodCalledOnSingleObject(nameof(IndexOf)); + + return Values.Select((item, index) => new { item, index }).FirstOrDefault(x => object.Equals(x.item, value)) + ?.index ?? -1; + } + /// /// 根据标识符获取值 /// @@ -375,6 +476,12 @@ public partial class Clay // 根据标识符查找 JsonNode 节点 var jsonNode = FindNode(identifier); + // 处理 object 类型生成 JsonElement 问题 + if (resultType == typeof(object)) + { + return DeserializeNode(jsonNode, Options); + } + return IsClay(resultType) ? new Clay(jsonNode, Options) : Helpers.DeserializeNode(jsonNode, resultType, jsonSerializerOptions ?? Options.JsonSerializerOptions); @@ -394,6 +501,94 @@ public partial class Clay public TResult? Get(object identifier, JsonSerializerOptions? jsonSerializerOptions = null) => (TResult?)Get(identifier, typeof(TResult), jsonSerializerOptions); + /// + /// 根据路径获取值 + /// + /// 不支持获取自定义委托。 + /// 带路径的标识符 + /// + /// + /// + public object? PathValue(string path) => PathValue(path); + + /// + /// 根据路径获取值 + /// + /// 不支持获取自定义委托。 + /// 带路径的标识符 + /// 转换的目标类型 + /// + /// + /// + /// + /// + /// + public object? PathValue(string path, Type resultType, JsonSerializerOptions? jsonSerializerOptions = null) + { + // 空检查 + ArgumentNullException.ThrowIfNull(path); + + // 根据路径分隔符进行分割,并确保至少有一个标识符 + var identifiers = path.Split(Options.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + if (identifiers is { Length: 0 }) + { + return null; + } + + // 根据标识符查找 JsonNode 节点 + var currentNode = FindNode(identifiers[0]); + if (currentNode is null) + { + return null; + } + + // 遍历剩余的标识符 + for (var i = 1; i < identifiers.Length; i++) + { + // 将 currentNode 转换为对象实例 + var currentValue = DeserializeNode(currentNode, Options); + + // 检查是否是 Clay 类型 + if (!IsClay(currentValue)) + { + throw new InvalidOperationException( + $"The identifier `{identifiers[i - 1]}` at path `{identifiers[i - 1]}:{identifiers[i]}` does not support further lookup."); + } + + // 进行下一级查找 + currentNode = ((Clay?)currentValue)?.FindNode(identifiers[i]); + if (currentNode is null) + { + return null; + } + } + + // 处理 object 类型生成 JsonElement 问题 + if (resultType == typeof(object)) + { + return DeserializeNode(currentNode, Options); + } + + return IsClay(resultType) + ? new Clay(currentNode, Options) + : Helpers.DeserializeNode(currentNode, resultType, jsonSerializerOptions ?? Options.JsonSerializerOptions); + } + + /// + /// 根据路径获取值 + /// + /// 不支持获取自定义委托。 + /// 带路径的标识符 + /// + /// + /// + /// 转换的目标类型 + /// + /// + /// + public TResult? PathValue(string path, JsonSerializerOptions? jsonSerializerOptions = null) => + (TResult?)PathValue(path, typeof(TResult), jsonSerializerOptions); + /// /// 根据标识符查找 节点 /// @@ -641,7 +836,7 @@ public partial class Clay throw new ArgumentException("Clay array contains one or more null elements.", nameof(clays)); } - // 检查是流变对象类型是否一致 + // 检查流变对象类型是否一致 if (clays.Any(u => u.Type != Type)) { throw new InvalidOperationException("All Clay objects must be of the same type."); @@ -668,6 +863,51 @@ public partial class Clay return combineClay; } + /// + /// 拓展属性或项 + /// + /// 值集合 + /// + /// + /// + /// + public Clay Extend(params object?[] values) + { + // 空检查 + ArgumentNullException.ThrowIfNull(values); + + // 检查是否是集合或数组 + if (IsArray) + { + AddRange(values); + + return this; + } + + // 遍历所有值 + foreach (var item in values) + { + // 检查值是否为空值或基本类型的值 + if (item is null || item.GetType().IsBasicType()) + { + throw new InvalidOperationException("Cannot extend a single object with null or basic type values."); + } + + // 将对象转换为字典集合 + var dictionary = item is Clay clayItem + ? clayItem.AsEnumerateObject().ToDictionary(object (u) => u.Key, u => u.Value) + : item.ObjectToDictionary(); + + // 遍历字典键值并设置 + foreach (var (key, value) in dictionary!) + { + this[key] = value; + } + } + + return this; + } + /// /// 根据标识符删除数据 /// @@ -733,8 +973,8 @@ public partial class Clay /// public object? As(Type resultType, JsonSerializerOptions? jsonSerializerOptions = null) { - // 检查是否是 Clay 类型或 IEnumerable 类型 - if (IsClay(resultType) || resultType == typeof(IEnumerable)) + // 检查是否是 Clay 类型或 IEnumerable 类型或 object 类型 + if (IsClay(resultType) || resultType == typeof(IEnumerable) || resultType == typeof(object)) { return this; } @@ -746,15 +986,23 @@ public partial class Clay } // 检查是否是 IEnumerable> 类型且是单一对象 - if (resultType == typeof(IEnumerable>) && IsObject) + if (typeof(IEnumerable>).IsAssignableFrom(resultType) && IsObject) { - return AsEnumerateObject(); + return resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Dictionary<,>) + ? AsEnumerateObject().ToDictionary(u => u.Key, u => u.Value) + : AsEnumerateObject(); } // 检查是否是 IEnumerable> 类型且是集合或数组 - if (resultType == typeof(IEnumerable>) && IsArray) + if (typeof(IEnumerable>).IsAssignableFrom(resultType) && IsArray) { - return AsEnumerateArray().Select((item, index) => new KeyValuePair(index, item)); + // 将流变对象转换为键值对集合 + var keyValuePairs = + AsEnumerateArray().Select((item, index) => new KeyValuePair(index, item)); + + return resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Dictionary<,>) + ? keyValuePairs.ToDictionary(u => u.Key, u => u.Value) + : keyValuePairs; } // 检查是否是 IActionResult 类型 @@ -926,7 +1174,8 @@ public partial class Clay /// /// /// - public static bool IsClay(object? obj) => obj is not null && IsClay(obj as Type ?? obj.GetType()); + public static bool IsClay([NotNullWhen(true)] object? obj) => + obj is not null && IsClay(obj as Type ?? obj.GetType()); /// /// 按照键升序排序并返回新的 @@ -945,7 +1194,7 @@ public partial class Clay // 初始化升序排序字典 var sorted = - new SortedDictionary(JsonCanvas.AsObject().ToDictionary()); + new SortedDictionary(JsonCanvas.AsObject().ToDictionary(), StringComparer.Ordinal); return Parse(sorted, options); } @@ -969,7 +1218,7 @@ public partial class Clay // 初始化降序排序字典 var sortedDesc = new SortedDictionary(Comparer.Create((x, y) => - string.Compare(y, x, StringComparison.InvariantCulture))); + string.Compare(y, x, StringComparison.Ordinal))); // 将 JsonCanvas 转换为 JsonObject 实例 var jsonObject = JsonCanvas.AsObject(); @@ -1031,6 +1280,64 @@ public partial class Clay return Rebuilt(Options.Configure(configure)); } + /// + /// 检查字符串是否是 JSON 对象({})或数组([]) + /// + /// 字符串 + /// 是否允许末尾多余逗号。默认值为:false。 + /// + /// + /// + public static bool IsJsonObjectOrArray(string? input, bool allowTrailingCommas = false) + { + // 检查输入是否为字符串类型,且字符串不是由空白字符组成 + if (input is null || string.IsNullOrWhiteSpace(input)) + { + return false; + } + + // 去除字符串两端空格 + var text = input.Trim(); + + // 检查字符串是否以 '{' 开头和 '}' 结尾,或者以 '[' 开头和 ']' 结尾 + if ((!text.StartsWith('{') || !text.EndsWith('}')) && (!text.StartsWith('[') || !text.EndsWith(']'))) + { + return false; + } + + try + { + // 使用 JsonDocument 解析字符串,若解析成功,说明是一个有效的 JSON 格式 + using var jsonDocument = JsonDocument.Parse(text, + new JsonDocumentOptions { AllowTrailingCommas = allowTrailingCommas }); + + return jsonDocument.RootElement.ValueKind is JsonValueKind.Object or JsonValueKind.Array; + } + catch (JsonException) + { + return false; + } + } + + /// + /// 将 实例通过转换管道传递并返回新的 (失败时抛出异常) + /// + /// 转换函数 + /// + /// + /// + /// + public Clay Pipe(Func transformer) => ExecuteTransformation(transformer, true); + + /// + /// 尝试将 实例通过转换管道传递,失败时返回原始对象 + /// + /// 转换函数 + /// + /// + /// + public Clay PipeTry(Func transformer) => ExecuteTransformation(transformer, false); + /// /// 单一对象 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Operator.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Operator.cs new file mode 100644 index 000000000..5449a651d --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Operator.cs @@ -0,0 +1,147 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.Shapeless; + +/// +/// 流变对象 +/// +public partial class Clay +{ + /// + public bool Equals(Clay? other) + { + // 检查是否是相同的实例 + if (ReferenceEquals(this, other)) + { + return true; + } + + // 空检查及基础类型检查 + if (other is null || Type != other.Type) + { + return false; + } + + return IsObject ? AreObjectEqual(this, other) : AreArrayEqual(this, other); + } + + /// + public override bool Equals(object? obj) => ReferenceEquals(this, obj) || Equals(obj as Clay); + + /// + /// 重载 == 运算符 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool operator ==(Clay? left, Clay? right) => Equals(left, right); + + /// + /// 重载 != 运算符 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool operator !=(Clay? left, Clay? right) => !(left == right); + + /// + public override int GetHashCode() + { + // 初始化 HashCode 实例 + var hash = new HashCode(); + + if (IsObject) + { + // 预处理键值对(排序) + var sortedEntries = AsEnumerateObject().OrderBy(kvp => kvp.Key, StringComparer.Ordinal); + + // 遍历键值对集合 + foreach (var (key, value) in sortedEntries) + { + // 递归计算键和值的哈希码 + hash.Add(key?.GetHashCode() ?? 0); + hash.Add(value?.GetHashCode() ?? 0); + } + } + else + { + // 遍历集合或数组集合 + foreach (var value in AsEnumerateArray()) + { + // 递归计算元素的哈希码 + hash.Add(value?.GetHashCode() ?? 0); + } + } + + return hash.ToHashCode(); + } + + /// + /// 检查两个单一对象实例是否相等 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static bool AreObjectEqual(Clay clay1, Clay clay2) => + clay1.Count == clay2.Count && clay1.All((dynamic? item) => + clay2.HasProperty(item?.Key) && object.Equals(item?.Value, clay2[item?.Key])); + + /// + /// 检查两个集合或数组实例是否相等 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static bool AreArrayEqual(Clay clay1, Clay clay2) + { + // 检查集合或数组长度是否相等 + if (clay1.Count != clay2.Count) + { + return false; + } + + // 遍历检查每一项是否相等 + for (var i = 0; i < clay1.Count; i++) + { + if (!Equals(clay1[i], clay2[i])) + { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Override.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Override.cs index 4c42df48b..6244e243c 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Override.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Override.cs @@ -42,6 +42,10 @@ public partial class Clay return csharpInvokeMemberBinderType.CreatePropertyGetter(typeArgumentsProperty); }); + /// + /// 可用于控制序列化时能够被序列化的标识符。 + public override IEnumerable GetDynamicMemberNames() => Keys.Select(u => u.ToString()!); + /// public override bool TryGetMember(GetMemberBinder binder, out object? result) { diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.cs index a282f6e15..7b8671614 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.cs @@ -21,7 +21,7 @@ namespace ThingsGateway.Shapeless; /// /// 流变对象 /// -public partial class Clay : DynamicObject, IEnumerable, IFormattable +public partial class Clay : DynamicObject, IEnumerable, IFormattable, IEquatable { /// /// @@ -535,6 +535,9 @@ public partial class Clay : DynamicObject, IEnumerable, IFormattable JsonNode jsonNode => jsonNode.DeepClone(), // 该操作不会复制自定义委托方法 Clay clay => clay.DeepClone(options).JsonCanvas, + // 排除 ExpandoObject 委托属性 + ExpandoObject expandoObject => SerializeToNode( + expandoObject.Where(kvp => kvp.Value is not Delegate).ToDictionary(u => u.Key, u => u.Value), options), _ => JsonSerializer.SerializeToNode(obj, options?.JsonSerializerOptions) }; @@ -709,6 +712,60 @@ public partial class Clay : DynamicObject, IEnumerable, IFormattable return true; } + /// + /// 执行 实例的核心转换逻辑,支持严格模式和容错模式 + /// + /// 转换函数 + /// + /// 模式开关: + /// - true:严格模式(失败抛出异常) + /// - false:容错模式(失败返回原对象) + /// + /// + /// + /// + /// + internal Clay ExecuteTransformation(Func transformer, bool strictMode) + { + // 空检查 + ArgumentNullException.ThrowIfNull(transformer); + + try + { + // 转换当前的流变对象 + var result = transformer(this); + + // 检查转换结果是否是有效的流变对象 + if (result is not null && IsClay((object?)result)) + { + return result; + } + + // 严格模式下抛出异常 + if (strictMode) + { + throw new InvalidOperationException( + "Transformation must return a non-null Clay object. The provided function either returned null or an incompatible type."); + } + + // 非严格模式下降级返回原对象 + return this; + } + catch (Exception ex) when (strictMode) + { + throw new InvalidOperationException( + "An unexpected error occurred during the transformation. Please verify the implementation of the transformation function.", + ex); + } + catch + { + // ignored + } + + // 非严格模式下降级返回原对象 + return this; + } + /// /// 抛出越界的数组索引异常 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Converters/ObjectToClayJsonConverter.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Converters/ObjectToClayJsonConverter.cs new file mode 100644 index 000000000..ed5df59fd --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Converters/ObjectToClayJsonConverter.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ThingsGateway.Shapeless; + +/// +/// JSON 序列化转换器 +/// +public sealed class ObjectToClayJsonConverter : JsonConverter +{ + /// + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // 将 Utf8JsonReader 转换为 JsonElement + var jsonElement = JsonElement.ParseValue(ref reader); + + // 检查 JSON 是否是对象或数组类型 + if (jsonElement.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + { + return Clay.Parse(jsonElement.ToString(), new ClayOptions { JsonSerializerOptions = options }); + } + + return jsonElement; + } + + /// + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, value.GetType(), options); +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessExtensions.cs new file mode 100644 index 000000000..af09c8d83 --- /dev/null +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessExtensions.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// 版权信息 +// 版权归百小僧及百签科技(广东)有限公司所有。 +// 所有权利保留。 +// 官方网站:https://baiqian.com +// +// 许可证信息 +// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 +// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 +// ------------------------------------------------------------------------ + +namespace ThingsGateway.Shapeless.Extensions; + +/// +/// 流变对象模块拓展类 +/// +public static class ShapelessExtensions +{ + /// + /// 将对象转换为 实例 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static Clay ToClay(this object? obj, ClayOptions? options = null) => Clay.Parse(obj, options); + + /// + /// 将对象转换为 实例 + /// + /// + /// + /// + /// 自定义配置委托 + /// + /// + /// + public static Clay ToClay(this object? obj, Action configure) => Clay.Parse(obj, configure); + + /// + /// 将 实例通过转换管道传递并返回新的 (失败时抛出异常) + /// + /// + /// + /// + /// 转换函数 + /// + /// + /// + /// + public static async Task PipeAsync(this Task clayTask, Func transformer) + { + var clay = await clayTask.ConfigureAwait(false); + return clay?.Pipe(transformer); + } + + /// + /// 尝试将 实例通过转换管道传递,失败时返回原始对象 + /// + /// + /// + /// + /// returns + /// + /// + /// + public static async Task PipeTryAsync(this Task clayTask, Func transformer) + { + var clay = await clayTask.ConfigureAwait(false); + return clay?.PipeTry(transformer); + } +} \ No newline at end of file diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessMvcBuilderExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessMvcBuilderExtensions.cs index 683ce9ec6..2412574a8 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessMvcBuilderExtensions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessMvcBuilderExtensions.cs @@ -20,6 +20,17 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ShapelessMvcBuilderExtensions { + /// + /// 添加 配置 + /// + /// + /// + /// + /// + /// + /// + public static IMvcBuilder AddClayOptions(this IMvcBuilder builder) => builder.AddClayOptions(_ => { }); + /// /// 添加 配置 /// diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Models/ClayEventArgs.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Models/ClayEventArgs.cs index 5f1ff90fa..1ce81235a 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Models/ClayEventArgs.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Models/ClayEventArgs.cs @@ -12,7 +12,7 @@ namespace ThingsGateway.Shapeless; /// -/// 对象事件数据 +/// 对象事件参数 /// public sealed class ClayEventArgs : EventArgs { diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Options/ClayOptions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Options/ClayOptions.cs index 90563d18c..fa0d9d854 100644 --- a/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Options/ClayOptions.cs +++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Options/ClayOptions.cs @@ -13,6 +13,8 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using ThingsGateway.Converters.Json; + namespace ThingsGateway.Shapeless; /// @@ -26,9 +28,14 @@ public sealed class ClayOptions public static ClayOptions Default => new(); /// - /// 允许访问缺失的属性或数组越界的 实例 + /// 允许属性名不区分大小写、访问缺失的属性或数组越界的 实例 /// - public static ClayOptions Flexible => new() { AllowMissingProperty = true, AllowIndexOutOfRange = true }; + public static ClayOptions Flexible => new() + { + PropertyNameCaseInsensitive = true, + AllowMissingProperty = true, + AllowIndexOutOfRange = true + }; /// /// 配置用于包裹非对象和非数组类型的键名 @@ -100,6 +107,12 @@ public sealed class ClayOptions /// public bool PropertyNameCaseInsensitive { get; set; } + /// + /// 路径分隔符 + /// + /// 默认值为:: + public string[] PathSeparator { get; set; } = [":"]; + /// /// 是否是只读模式 /// @@ -112,11 +125,20 @@ public sealed class ClayOptions public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(JsonSerializerOptions.Default) { PropertyNameCaseInsensitive = true, + // 允许 String 转 Number NumberHandling = JsonNumberHandling.AllowReadingFromString, // 解决中文乱码问题 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, AllowTrailingCommas = true, - Converters = { new ClayJsonConverter() } + Converters = + { + new ClayJsonConverter(), + new ObjectToClayJsonConverter(), + new DateTimeConverterUsingDateTimeParseAsFallback(), + new DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback(), + // 允许 Number 或 Boolean 转 String + new StringJsonConverter() + } }; /// diff --git a/src/Admin/ThingsGateway.Razor/ThingsGateway.Razor.csproj b/src/Admin/ThingsGateway.Razor/ThingsGateway.Razor.csproj index 78c08f28c..0fafebb6e 100644 --- a/src/Admin/ThingsGateway.Razor/ThingsGateway.Razor.csproj +++ b/src/Admin/ThingsGateway.Razor/ThingsGateway.Razor.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 270a02ae1..c989a1070 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,8 +1,8 @@ - 10.5.19 - 10.5.19 + 10.6.0 + 10.6.0 2.1.7 diff --git a/src/Foundation/ThingsGateway.CSScript/CSharpScriptEngineExtension.cs b/src/Foundation/ThingsGateway.CSScript/CSharpScriptEngineExtension.cs index d0dbe383a..6d0927903 100644 --- a/src/Foundation/ThingsGateway.CSScript/CSharpScriptEngineExtension.cs +++ b/src/Foundation/ThingsGateway.CSScript/CSharpScriptEngineExtension.cs @@ -62,7 +62,6 @@ public static class CSharpScriptEngineExtension { item?.Value?.TryDispose(); item?.Value?.GetType().Assembly.Unload(); - GC.Collect(); } } catch @@ -130,7 +129,6 @@ public static class CSharpScriptEngineExtension {_using} {_body} "); - GC.Collect(); Instance.Set(field, runScript); } } diff --git a/src/Foundation/ThingsGateway.CSScript/ExpressionEvaluatorExtension.cs b/src/Foundation/ThingsGateway.CSScript/ExpressionEvaluatorExtension.cs index dae90b075..26c8dd658 100644 --- a/src/Foundation/ThingsGateway.CSScript/ExpressionEvaluatorExtension.cs +++ b/src/Foundation/ThingsGateway.CSScript/ExpressionEvaluatorExtension.cs @@ -78,7 +78,6 @@ public static class ExpressionEvaluatorExtension { item?.Value?.TryDispose(); item?.Value?.GetType().Assembly.Unload(); - GC.Collect(); } } catch @@ -140,7 +139,6 @@ public static class ExpressionEvaluatorExtension }} }} "); - GC.Collect(); Instance.Set(field, runScript); } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/Plugin/PluginService.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/Plugin/PluginService.cs index 079f6a9cb..6d223eb86 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/Plugin/PluginService.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/Plugin/PluginService.cs @@ -351,7 +351,6 @@ internal sealed class PluginService : IPluginService { item.Value.AssemblyLoadContext.Unload(); } - GC.Collect(); ClearCache(); _assemblyLoadContextDict.Clear(); } @@ -619,7 +618,6 @@ internal sealed class PluginService : IPluginService _assemblyLoadContextDict.Remove(path); //卸载 assemblyLoadContext.AssemblyLoadContext.Unload(); - GC.Collect(); } } diff --git a/src/Version.props b/src/Version.props index 8bfa52615..b72d9eb58 100644 --- a/src/Version.props +++ b/src/Version.props @@ -1,6 +1,6 @@ - 10.5.19 + 10.6.0