Compare commits

...

8 Commits

Author SHA1 Message Date
Diego
f1e78a0e8a 恢复配置json 2025-05-15 12:17:43 +08:00
Diego
0bf28ec275 更新语言资源 2025-05-15 12:17:09 +08:00
Diego
41f8412c97 支持达梦数据库 2025-05-15 12:15:32 +08:00
Diego
c535974362 更新规则引擎示例 2025-05-15 10:44:12 +08:00
Diego
1860c5f215 build:10.6.1 2025-05-15 09:12:33 +08:00
Diego
6d778b2d39 增加initDatabase配置项 2025-05-15 09:08:08 +08:00
Diego
f48b99c259 更新示例 2025-05-14 21:12:51 +08:00
Diego
3c73b93051 更新依赖 2025-05-14 18:52:19 +08:00
146 changed files with 3371 additions and 813 deletions

View File

@@ -169,7 +169,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void RemoveAllClientId() public void RemoveAllClientId()
{ {
using var db = GetDB(); using var db = GetDB();
db.Updateable<VerificatInfo>().SetColumns("ClientIds", new ConcurrentList<long>().ToJsonNetString()).Where(a => a.Id >= 0).ExecuteCommand(); db.Updateable<VerificatInfo>().SetColumns(a=>a.ClientIds==null).Where(a => a.Id > 0).ExecuteCommand();
VerificatInfoService.RemoveCache(); VerificatInfoService.RemoveCache();
} }

View File

@@ -24,6 +24,11 @@ public sealed class SqlSugarOption : ConnectionConfig
/// </summary> /// </summary>
public bool InitSeedData { get; set; } = false; public bool InitSeedData { get; set; } = false;
/// <summary>
/// 初始化数据库
/// </summary>
public bool InitDatabase { get; set; } = false;
/// <summary> /// <summary>
/// 初始化表 /// 初始化表
/// </summary> /// </summary>

View File

@@ -89,7 +89,7 @@ public class Startup : AppStartup
DbContext.DbConfigs?.ForEach(it => DbContext.DbConfigs?.ForEach(it =>
{ {
var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象 var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
if (it.InitTable == true) if (it.InitDatabase == true)
connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建 connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
}); });

View File

@@ -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;
/// <summary>
/// HostApplication 拓展
/// </summary>
public static class AppHostApplicationBuilderExtensions
{
/// <summary>
/// Host 应用注入
/// </summary>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="autoRegisterBackgroundService"></param>
/// <returns>HostApplicationBuilder</returns>
public static HostApplicationBuilder Inject(this HostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
{
// 初始化配置
InternalApp.ConfigureApplication(hostApplicationBuilder, autoRegisterBackgroundService);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="options">组件参数</param>
/// <returns></returns>
public static HostApplicationBuilder AddComponent<TComponent>(this HostApplicationBuilder hostApplicationBuilder, object options = default)
where TComponent : class, IServiceComponent, new()
{
hostApplicationBuilder.Services.AddComponent<TComponent>(options);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
/// <typeparam name="TComponentOptions">组件参数</typeparam>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="options">组件参数</param>
/// <returns><see cref="HostApplicationBuilder"/></returns>
public static HostApplicationBuilder AddComponent<TComponent, TComponentOptions>(this HostApplicationBuilder hostApplicationBuilder, TComponentOptions options = default)
where TComponent : class, IServiceComponent, new()
{
hostApplicationBuilder.Services.AddComponent<TComponent, TComponentOptions>(options);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="componentType">组件类型</param>
/// <param name="options">组件参数</param>
/// <returns><see cref="HostApplicationBuilder"/></returns>
public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, Type componentType, object options = default)
{
hostApplicationBuilder.Services.AddComponent(componentType, options);
return hostApplicationBuilder;
}
}

View File

@@ -467,18 +467,20 @@ public static class ObjectExtensions
return obj; return obj;
} }
/// <summary> /// <summary>
/// 查找方法指定特性,如果没找到则继续查找声明类 /// 查找方法指定特性,如果没找到则继续查找声明类
/// </summary> /// </summary>
/// <typeparam name="TAttribute"></typeparam> /// <typeparam name="TAttribute"></typeparam>
/// <param name="method"></param> /// <param name="method"></param>
/// <param name="inherit"></param> /// <param name="inherit"></param>
/// <param name="searchFromReflectedType">searchFromRuntimeType</param>
/// <returns></returns> /// <returns></returns>
internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit) internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit, bool searchFromReflectedType = false)
where TAttribute : Attribute where TAttribute : Attribute
{ {
// 获取方法所在类型 // 获取方法所在类型
var declaringType = method.DeclaringType; var declaringType = !searchFromReflectedType ? method.DeclaringType : method.ReflectedType; // 解决嵌套继承问题
var attributeType = typeof(TAttribute); var attributeType = typeof(TAttribute);
@@ -493,7 +495,6 @@ public static class ObjectExtensions
return foundAttribute; return foundAttribute;
} }
/// <summary> /// <summary>
/// 格式化字符串 /// 格式化字符串
/// </summary> /// </summary>

View File

@@ -132,6 +132,34 @@ internal static class InternalApp
}); });
} }
/// <summary>
/// 配置 Furion 框架(非 Web
/// </summary>
/// <param name="hostApplicationBuilder"></param>
/// <param name="autoRegisterBackgroundService"></param>
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<GenericHostLifetimeEventsHostedService>();
// 初始化应用服务
hostApplicationBuilder.Services.AddApp();
// 自动注册 BackgroundService
if (autoRegisterBackgroundService) hostApplicationBuilder.Services.AddAppHostedService();
}
/// <summary> /// <summary>
/// 自动装载主机配置 /// 自动装载主机配置
/// </summary> /// </summary>

View File

@@ -9,7 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@@ -29,34 +29,36 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns> /// <returns></returns>
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(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
aesAlg.Mode = mode; aesAlg.Mode = mode;
aesAlg.Padding = padding; aesAlg.Padding = padding;
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB) 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 msEncrypt = new MemoryStream();
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) 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); swEncrypt.Write(text);
} }
var encryptedContent = msEncrypt.ToArray(); var encryptedContent = msEncrypt.ToArray();
// 如果是 CBC 模式,将 IV 和密文拼接在一起 // 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB && iv == null)
{ {
var result = new byte[aesAlg.IV.Length + encryptedContent.Length]; var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length); Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
@@ -76,35 +78,43 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns> /// <returns></returns>
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 fullCipher = Convert.FromBase64String(hash);
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
var bKey = Encoding.UTF8.GetBytes(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(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
aesAlg.Mode = mode; aesAlg.Mode = mode;
aesAlg.Padding = padding; aesAlg.Padding = padding;
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB)
{ {
var bVector = new byte[16]; if (iv == null)
var cipher = new byte[fullCipher.Length - bVector.Length]; {
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); iv = new byte[aesAlg.BlockSize / 8];
Unsafe.CopyBlock(ref cipher[0], ref fullCipher[bVector.Length], (uint)(fullCipher.Length - bVector.Length)); var cipher = new byte[fullCipher.Length - iv.Length];
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
aesAlg.IV = iv ?? bVector; Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
fullCipher = cipher; 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 msDecrypt = new MemoryStream(fullCipher);
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); 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(); return srDecrypt.ReadToEnd();
} }
@@ -117,19 +127,13 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>加密后的字节数组</returns> /// <returns>加密后的字节数组</returns>
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 bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
var keyBytes = Encoding.UTF8.GetBytes(skey); if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
iv ??= GenerateRandomIV(); // 生成随机 IV
}
using var aesAlg = Aes.Create(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
@@ -138,34 +142,29 @@ public class AESEncryption
if (mode != CipherMode.ECB) 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 memoryStream = new MemoryStream();
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Write); using (var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(), CryptoStreamMode.Write))
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
// 如果是 CBC 模式,将 IV 和密文拼接在一起
if (mode != CipherMode.ECB)
{ {
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(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; return result;
} }
// 如果是 ECB 模式,直接返回密文 return encryptedContent;
return memoryStream.ToArray();
}
// 生成随机 IV
private static byte[] GenerateRandomIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
} }
/// <summary> /// <summary>
@@ -176,25 +175,13 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns> /// <returns></returns>
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 bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
var keyBytes = Encoding.UTF8.GetBytes(skey); if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
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();
}
}
using var aesAlg = Aes.Create(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
@@ -203,21 +190,36 @@ public class AESEncryption
if (mode != CipherMode.ECB) 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; aesAlg.IV = iv;
} }
using var memoryStream = new MemoryStream(bytes); 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(); using var originalStream = new MemoryStream();
var buffer = new byte[1024]; cryptoStream.CopyTo(originalStream);
var readBytes = 0;
while ((readBytes = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
{
originalStream.Write(buffer, 0, readBytes);
}
return originalStream.ToArray(); return originalStream.ToArray();
} }
/// <summary>
/// 生成随机 IV
/// </summary>
/// <returns></returns>
private static byte[] GenerateRandomIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
}
} }

View File

@@ -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;
/// <summary>
/// GZip 压缩解压
/// </summary>
[SuppressSniffer]
public static class GzipEncryption
{
/// <summary>
/// 压缩字符串并返回字节数组
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
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();
}
/// <summary>
/// 从字节数组解压
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
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());
}
/// <summary>
/// 压缩字符串并返回 Base64 字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
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());
}
/// <summary>
/// 从 Base64 字符串解压
/// </summary>
/// <param name="base64String"></param>
/// <returns></returns>
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());
}
}

View File

@@ -77,10 +77,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
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);
} }
/// <summary> /// <summary>
@@ -91,10 +92,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
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);
} }
/// <summary> /// <summary>
@@ -105,10 +107,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
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);
} }
/// <summary> /// <summary>
@@ -119,10 +122,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
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);
} }
/// <summary> /// <summary>
@@ -243,4 +247,44 @@ public static class StringEncryptionExtensions
{ {
return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength); return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength);
} }
/// <summary>
/// Gzip 压缩字符串并返回字节数组
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static byte[] ToGzipCompress(this string text)
{
return GzipEncryption.Compress(text);
}
/// <summary>
/// Gzip 从字节数组解压
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string ToGzipDecompress(this byte[] bytes)
{
return GzipEncryption.Decompress(bytes);
}
/// <summary>
/// Gzip 压缩字符串并返回 Base64 字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ToGzipCompressToBase64(this string text)
{
return GzipEncryption.CompressToBase64(text);
}
/// <summary>
/// Gzip 从 Base64 字符串解压
/// </summary>
/// <param name="base64String"></param>
/// <returns></returns>
public static string ToGzipDecompressFromBase64(this string base64String)
{
return GzipEncryption.DecompressFromBase64(base64String);
}
} }

View File

@@ -565,10 +565,10 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase(); if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase();
// 判断是否贴有任何 [FromXXX] 特性了 // 判断是否贴有任何 [FromXXX] 特性了
var hasFormAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType())); var hasFromAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
// 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性 // 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性
if (isQueryParametersAction && !hasFormAttribute) if (isQueryParametersAction && !hasFromAttribute)
{ {
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() }); parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
continue; continue;
@@ -577,7 +577,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过 // 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过
// 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过 // 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过
if (!parameterAttributes.Any(u => u is FromRouteAttribute) if (!parameterAttributes.Any(u => u is FromRouteAttribute)
&& (!parameterType.IsRichPrimitive() || hasFormAttribute)) continue; && (!parameterType.IsRichPrimitive() || hasFromAttribute)) continue;
// 处理基元数组数组类型,还有全局配置参数问题 // 处理基元数组数组类型,还有全局配置参数问题
if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray) if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray)
@@ -588,7 +588,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 处理 [ApiController] 特性情况 // 处理 [ApiController] 特性情况
// https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference // 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])则跳过 // 处理默认基元参数绑定方式,若是 query[FromQuery])则跳过
if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query") if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query")

View File

@@ -84,8 +84,6 @@ internal sealed class DynamicApiRuntimeChangeProvider : IDynamicApiRuntimeChange
if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart); if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart);
} }
GC.Collect();
GC.WaitForPendingFinalizers();
} }
} }

View File

@@ -72,7 +72,7 @@ public sealed class EventBusOptionsBuilder
/// <summary> /// <summary>
/// 是否启用执行完成触发 GC 回收 /// 是否启用执行完成触发 GC 回收
/// </summary> /// </summary>
public bool GCCollect { get; set; } = true; public bool GCCollect { get; set; } = false;
/// <summary> /// <summary>
/// 是否启用日志记录 /// 是否启用日志记录

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Reflection; using System.Reflection;
using System.Text.Json;
namespace ThingsGateway.EventBus; namespace ThingsGateway.EventBus;
@@ -57,4 +58,31 @@ public abstract class EventHandlerContext
/// </summary> /// </summary>
/// <remarks><remarks>如果是动态订阅,可能为 null</remarks></remarks> /// <remarks><remarks>如果是动态订阅,可能为 null</remarks></remarks>
public EventSubscribeAttribute Attribute { get; } public EventSubscribeAttribute Attribute { get; }
private static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerOptions.Default)
{
PropertyNameCaseInsensitive = true
};
/// <summary>
/// 获取负载数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T GetPayload<T>()
{
var rawPayload = Source.Payload;
if (rawPayload is null)
{
return default;
}
else if (rawPayload is JsonElement jsonElement)
{
return JsonSerializer.Deserialize<T>(jsonElement.GetRawText(), JsonSerializerOptions);
}
else
{
return (T)rawPayload;
}
}
} }

View File

@@ -38,4 +38,18 @@ public sealed class EventHandlerExecutingContext : EventHandlerContext
/// 执行前时间 /// 执行前时间
/// </summary> /// </summary>
public DateTime ExecutingTime { get; internal set; } public DateTime ExecutingTime { get; internal set; }
/// <summary>
/// 执行结果
/// </summary>
internal object Result { get; private set; }
/// <summary>
/// 设置执行结果
/// </summary>
/// <param name="result"></param>
public void SetResult(object result)
{
Result = result;
}
} }

View File

@@ -42,4 +42,10 @@ public sealed class EventHandlerEventArgs : EventArgs
/// 异常信息 /// 异常信息
/// </summary> /// </summary>
public Exception Exception { get; internal set; } public Exception Exception { get; internal set; }
/// <summary>
/// 执行结果
/// </summary>
public object Result { get; internal set; }
} }

View File

@@ -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) catch (Exception ex)
{ {

View File

@@ -198,8 +198,9 @@ public class JWTEncryption
/// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param> /// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param>
/// <param name="tokenPrefix"></param> /// <param name="tokenPrefix"></param>
/// <param name="clockSkew"></param> /// <param name="clockSkew"></param>
/// <param name="onRefreshing">当刷新时触发</param>
/// <returns></returns> /// <returns></returns>
public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5) public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action<string, string> onRefreshing = null)
{ {
// 如果验证有效,则跳过刷新 // 如果验证有效,则跳过刷新
if (context.User.Identity.IsAuthenticated) if (context.User.Identity.IsAuthenticated)
@@ -245,7 +246,11 @@ public class JWTEncryption
// 返回新的 Token // 返回新的 Token
httpContext.Response.Headers[accessTokenKey] = accessToken; httpContext.Response.Headers[accessTokenKey] = accessToken;
// 返回新的 刷新Token // 返回新的 刷新Token
httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); var refreshAccessToken = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); ;
httpContext.Response.Headers[xAccessTokenKey] = refreshAccessToken;
// 调用刷新后回调函数
onRefreshing?.Invoke(accessToken, refreshAccessToken);
// 处理 axios 问题 // 处理 axios 问题
httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs); httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs);

View File

@@ -90,7 +90,8 @@ public sealed class ConsoleFormatterExtend : ConsoleFormatter, IDisposable
, true , true
, _disableColors , _disableColors
, _formatterOptions.WithTraceId , _formatterOptions.WithTraceId
, _formatterOptions.WithStackFrame); , _formatterOptions.WithStackFrame
, _formatterOptions.FormatProvider);
} }
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件 // 判断是否自定义了日志筛选器,如果是则检查是否符合条件

View File

@@ -12,6 +12,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Console;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -69,4 +71,10 @@ public sealed class ConsoleFormatterExtendOptions : ConsoleFormatterOptions
/// 日志消息内容转换(如脱敏处理) /// 日志消息内容转换(如脱敏处理)
/// </summary> /// </summary>
public Func<string, string> MessageProcess { get; set; } public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -118,7 +118,7 @@ public sealed class DatabaseLogger : ILogger
// 设置日志消息模板 // 设置日志消息模板
logMsg.Message = _options.MessageFormat != null logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg) ? _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) if (logMsg.Message is null)

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -80,4 +82,10 @@ public sealed class DatabaseLoggerOptions
/// 日志消息内容转换(如脱敏处理) /// 日志消息内容转换(如脱敏处理)
/// </summary> /// </summary>
public Func<string, string> MessageProcess { get; set; } public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -116,7 +116,7 @@ public sealed class FileLogger : ILogger
// 设置日志消息模板 // 设置日志消息模板
logMsg.Message = _options.MessageFormat != null logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg) ? _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) if (logMsg.Message is null)

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -104,4 +106,10 @@ public sealed class FileLoggerOptions
/// 日志消息内容转换(如脱敏处理) /// 日志消息内容转换(如脱敏处理)
/// </summary> /// </summary>
public Func<string, string> MessageProcess { get; set; } public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -120,6 +122,6 @@ public struct LogMessage
/// <returns><see cref="string"/></returns> /// <returns><see cref="string"/></returns>
public override readonly string ToString() public override readonly string ToString()
{ {
return Penetrates.OutputStandardMessage(this); return Penetrates.OutputStandardMessage(this, provider: CultureInfo.InvariantCulture);
} }
} }

View File

@@ -192,7 +192,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
/// <param name="claimsPrincipal"></param> /// <param name="claimsPrincipal"></param>
/// <param name="authorization"></param> /// <param name="authorization"></param>
/// <returns></returns> /// <returns></returns>
private static List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization) private List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
{ {
var templates = new List<string>(); var templates = new List<string>();
@@ -219,7 +219,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
var succeed = long.TryParse(value, out var seconds); var succeed = long.TryParse(value, out var seconds);
if (succeed) 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)";
} }
} }

View File

@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
@@ -143,4 +144,11 @@ public sealed class LoggingMonitorSettings
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
SkipValidation = true SkipValidation = true
}; };
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -107,13 +107,15 @@ internal static class Penetrates
/// <param name="isConsole"></param> /// <param name="isConsole"></param>
/// <param name="withTraceId"></param> /// <param name="withTraceId"></param>
/// <param name="withStackFrame"></param> /// <param name="withStackFrame"></param>
/// <param name="provider"></param>
/// <returns></returns> /// <returns></returns>
internal static string OutputStandardMessage(LogMessage logMsg internal static string OutputStandardMessage(LogMessage logMsg
, string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd" , string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd"
, bool isConsole = false , bool isConsole = false
, bool disableColors = true , bool disableColors = true
, bool withTraceId = false , bool withTraceId = false
, bool withStackFrame = false) , bool withStackFrame = false
, IFormatProvider? provider = null)
{ {
// 空检查 // 空检查
if (logMsg.Message is null) return null; if (logMsg.Message is null) return null;
@@ -127,7 +129,7 @@ internal static class Penetrates
_ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors); _ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors);
formatString.Append(": "); formatString.Append(": ");
formatString.Append(logMsg.LogDateTime.ToString(dateFormat)); formatString.Append(logMsg.LogDateTime.ToString(dateFormat, provider));
formatString.Append(' '); formatString.Append(' ');
formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L"); formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L");
formatString.Append(' '); formatString.Append(' ');

View File

@@ -78,9 +78,9 @@ public partial interface ISchedulerFactory
/// <returns><see cref="IJob"/></returns> /// <returns><see cref="IJob"/></returns>
IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context); IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context);
/// <summary> ///// <summary>
/// GC 垃圾回收器回收处理 ///// GC 垃圾回收器回收处理
/// </summary> ///// </summary>
/// <remarks>避免频繁 GC 回收</remarks> ///// <remarks>避免频繁 GC 回收</remarks>
void GCCollect(); //void GCCollect();
} }

View File

@@ -183,9 +183,10 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
// 标记当前方法初始化完成 // 标记当前方法初始化完成
PreloadCompleted = true; PreloadCompleted = true;
// 释放引用内存并立即回收GC // 释放引用内存
_schedulerBuilders.Clear(); _schedulerBuilders.Clear();
GCCollect();
//GCCollect();
// 输出作业调度器初始化日志 // 输出作业调度器初始化日志
if (!preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count); 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; return jobHandler;
} }
/// <summary> ///// <summary>
/// GC 垃圾回收器回收处理 ///// GC 垃圾回收器回收处理
/// </summary> ///// </summary>
/// <remarks>避免频繁 GC 回收</remarks> ///// <remarks>避免频繁 GC 回收</remarks>
public void GCCollect() //public void GCCollect()
{ //{
var nowTime = DateTime.UtcNow; // var nowTime = DateTime.UtcNow;
if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS)) // if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
{ // {
LastGCCollectTime = nowTime; // LastGCCollectTime = nowTime;
// 通知 GC 垃圾回收器立即回收 // // 通知 GC 垃圾回收器立即回收
GC.Collect(); // GC.Collect();
GC.WaitForPendingFinalizers(); // GC.WaitForPendingFinalizers();
} // }
} //}
/// <summary> /// <summary>
/// 释放非托管资源 /// 释放非托管资源
@@ -535,7 +536,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
//_logger.LogWarning("Schedule hosted service cancels hibernation."); //_logger.LogWarning("Schedule hosted service cancels hibernation.");
// 通知 GC 垃圾回收器立即回收 // 通知 GC 垃圾回收器立即回收
GCCollect(); //GCCollect();
}); });
} }

View File

@@ -389,7 +389,7 @@ internal sealed class ScheduleHostedService : BackgroundService
_jobCancellationToken.Cancel(jobId, triggerId, false); _jobCancellationToken.Cancel(jobId, triggerId, false);
// 通知 GC 垃圾回收器回收 // 通知 GC 垃圾回收器回收
_schedulerFactory.GCCollect(); //_schedulerFactory.GCCollect();
} }
}, stoppingToken); }, stoppingToken);
}); });

View File

@@ -113,10 +113,8 @@ public static class SpecificationDocumentBuilder
} }
// 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口 // 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口
var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true); var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true, true);
var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true, true);
var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true);
if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false; if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false;
if (currentGroup == AllGroupsKey) if (currentGroup == AllGroupsKey)

View File

@@ -433,10 +433,15 @@ public partial class Crontab
{ {
newValue = newValue.AddSeconds(-newValue.Second); 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<ITimeParser>().ToList(); var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomMinute = minuteParsers.OfType<RandomParser>().Any();
var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList(); var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomHour = hourParsers.OfType<RandomParser>().Any();
// 获取秒、分钟、小时解析器中最小起始值 // 获取秒、分钟、小时解析器中最小起始值
// 该值主要用来获取下一个发生值的输入参数 // 该值主要用来获取下一个发生值的输入参数
@@ -456,7 +461,7 @@ public partial class Crontab
{ {
// 获取秒所有字符解析器 // 获取秒所有字符解析器
var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList(); var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomSecond = secondParsers.OfType<RandomParser>().Any();
// 获取秒解析器最小起始值 // 获取秒解析器最小起始值
firstSecondValue = secondParsers.Select(x => x.First()).Min(); 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, newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours,
overflow ? firstMinuteValue : newMinutes, overflow && !randomMinute ? firstMinuteValue : newMinutes,
overflow ? firstSecondValue : newSeconds); overflow && !randomSecond ? firstSecondValue : newSeconds);
// 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器 // 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue)) if (!overflow && !IsMatch(newValue))
@@ -534,7 +539,7 @@ public partial class Crontab
} }
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间 // 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间
if (!overflow) if (!randomHour && !overflow)
{ {
return MinDate(newValue, endTime); return MinDate(newValue, endTime);
} }
@@ -788,8 +793,15 @@ public partial class Crontab
/// <param name="defaultValue">默认值</param> /// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param> /// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns> /// <returns><see cref="int"/></returns>
private static int Increment(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow) private static int Increment(List<ITimeParser> 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)) var nextValue = parsers.Select(x => x.Next(value))
.Where(x => x > value) .Where(x => x > value)
.Min() .Min()
@@ -808,7 +820,7 @@ public partial class Crontab
/// <param name="defaultValue">默认值</param> /// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param> /// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns> /// <returns><see cref="int"/></returns>
private static int Decrement(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow) private static int Decrement(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{ {
var previousValue = parsers.Select(x => x.Previous(value)) var previousValue = parsers.Select(x => x.Previous(value))
.Where(x => x < value) .Where(x => x < value)

View File

@@ -69,7 +69,7 @@ internal sealed class RandomParser : ICronParser, ITimeParser
/// <returns><see cref="bool"/></returns> /// <returns><see cref="bool"/></returns>
public bool IsMatch(DateTime datetime) public bool IsMatch(DateTime datetime)
{ {
return true; return Kind is not CrontabFieldKind.Hour;
} }
/// <summary> /// <summary>

View File

@@ -168,7 +168,7 @@ public static class UnifyContext
if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null; if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null;
// 获取序列化配置 // 获取序列化配置
var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true); var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true, true);
if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null; if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null;
// 解析全局配置 // 解析全局配置
@@ -225,7 +225,8 @@ public static class UnifyContext
|| method.GetRealReturnType().HasImplementedRawGeneric(unityMetadata.ResultType) || 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.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.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) 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.CustomAttributes.Any(x => typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
&& method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true) && 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; unifyResult = isSkip ? null : App.RootServices.GetService(unityMetadata.ProviderType) as IUnifyResultProvider;
return unifyResult == null || isSkip; return unifyResult == null || isSkip;
@@ -398,7 +400,7 @@ public static class UnifyContext
{ {
if (method == default) return default; if (method == default) return default;
var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true); var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true, true);
// 获取元数据 // 获取元数据
var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata); var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata);

View File

@@ -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;
/// <summary>
/// <see cref="DateTime" /> JSON 序列化转换器
/// </summary>
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTime" /> 时间使用 <c>DateTime.Parse</c> 作为回退。</remarks>
public sealed class DateTimeConverterUsingDateTimeParseAsFallback : JsonConverter<DateTime>
{
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value);
}

View File

@@ -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;
/// <summary>
/// <see cref="DateTimeOffset" /> JSON 序列化转换器
/// </summary>
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTimeOffset" /> 时间使用 <c>DateTimeOffset.Parse</c> 作为回退。</remarks>
public sealed class DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback : JsonConverter<DateTimeOffset>
{
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value);
}

View File

@@ -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;
/// <summary>
/// <see cref="string" /> JSON 序列化转换器
/// </summary>
/// <remarks>解决 Number 类型和 Boolean 类型转 String 类型时异常。</remarks>
public sealed class StringJsonConverter : JsonConverter<string>
{
/// <inheritdoc />
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()
};
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
writer.WriteStringValue(value);
}

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
namespace ThingsGateway.Extensions; namespace ThingsGateway.Extensions;
@@ -43,7 +44,7 @@ internal static class LinqExpressionExtensions
} }
/// <summary> /// <summary>
/// 解析表达式属性名称 /// 解析表达式并获取属性的 <see cref="PropertyInfo" /> 实例
/// </summary> /// </summary>
/// <typeparam name="T">对象类型</typeparam> /// <typeparam name="T">对象类型</typeparam>
/// <typeparam name="TProperty">属性类型</typeparam> /// <typeparam name="TProperty">属性类型</typeparam>
@@ -51,48 +52,54 @@ internal static class LinqExpressionExtensions
/// <see cref="Expression{TDelegate}" /> /// <see cref="Expression{TDelegate}" />
/// </param> /// </param>
/// <returns> /// <returns>
/// <see cref="string" /> /// <see cref="PropertyInfo" />
/// </returns> /// </returns>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
internal static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) => internal static PropertyInfo GetProperty<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) =>
propertySelector.Body switch propertySelector.Body switch
{ {
// 检查 Lambda 表达式的主体是否是 MemberExpression 类型 // 检查 Lambda 表达式的主体是否是 MemberExpression 类型
MemberExpression memberExpression => GetPropertyName<T>(memberExpression), MemberExpression memberExpression => GetProperty<T>(memberExpression),
// 如果主体是 UnaryExpression 类型,则继续解析 // 如果主体是 UnaryExpression 类型,则继续解析
UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetPropertyName<T>( UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetProperty<T>(
nestedMemberExpression), nestedMemberExpression),
_ => throw new ArgumentException("Expression must be a simple member access (e.g. x => x.Property).",
_ => throw new ArgumentException("Expression is not valid for property selection.") nameof(propertySelector))
}; };
/// <summary> /// <summary>
/// 解析表达式属性名称 /// 从成员表达式中提取 <see cref="PropertyInfo" /> 实例
/// </summary> /// </summary>
/// <typeparam name="T">对象类型</typeparam>
/// <param name="memberExpression"> /// <param name="memberExpression">
/// <see cref="MemberExpression" /> /// <see cref="MemberExpression" />
/// </param> /// </param>
/// <typeparam name="T">对象类型</typeparam>
/// <returns> /// <returns>
/// <see cref="string" /> /// <see cref="PropertyInfo" />
/// </returns> /// </returns>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
internal static string GetPropertyName<T>(MemberExpression memberExpression) internal static PropertyInfo GetProperty<T>(MemberExpression memberExpression)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(memberExpression); ArgumentNullException.ThrowIfNull(memberExpression);
// 获取属性声明类型 // 确保表达式根是 T 类型的参数
var propertyType = memberExpression.Member.DeclaringType; if (memberExpression.Expression is not ParameterExpression parameterExpression ||
parameterExpression.Type != typeof(T))
// 检查是否越界访问属性
if (propertyType != 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;
} }
} }

View File

@@ -11,6 +11,7 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@@ -149,7 +150,7 @@ internal static partial class StringExtensions
var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators); var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators);
return (from pair in pairs return (from pair in pairs
select pair.Split('=') select pair.Split('=', 2) // 限制只分割一次
into keyValue into keyValue
where keyValue.Length == 2 where keyValue.Length == 2
select new KeyValuePair<string, string?>(keyValue[0].Trim(), keyValue[1])).ToList(); select new KeyValuePair<string, string?>(keyValue[0].Trim(), keyValue[1])).ToList();
@@ -328,6 +329,18 @@ internal static partial class StringExtensions
}); });
} }
/// <summary>
/// 转换输入字符串中的任何转义字符
/// </summary>
/// <param name="input">
/// <see cref="string" />
/// </param>
/// <returns>
/// <see cref="string" />
/// </returns>
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
/// <summary> /// <summary>
/// 占位符匹配正则表达式 /// 占位符匹配正则表达式
/// </summary> /// </summary>

View File

@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Buffers;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace ThingsGateway.Extensions; namespace ThingsGateway.Extensions;
@@ -34,4 +36,17 @@ internal static class Utf8JsonReaderExtensions
return jsonDocument.RootElement.Clone().GetRawText(); return jsonDocument.RootElement.Clone().GetRawText();
} }
/// <summary>
/// 从 <see cref="Utf8JsonReader" /> 中提取原始值,并将其转换为字符串
/// </summary>
/// <remarks>支持处理各种类型的原始值(例如数字、布尔值等)。</remarks>
/// <param name="reader">
/// <see cref="Utf8JsonReader" />
/// </param>
/// <returns>
/// <see cref="string" />
/// </returns>
internal static string ConvertRawValueToString(this Utf8JsonReader reader) =>
Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);
} }

View File

@@ -97,16 +97,45 @@ internal static class V5_ObjectExtensions
case ICollection collection: case ICollection collection:
count = collection.Count; count = collection.Count;
return true; 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 属性 // 反射查找是否存在 Count 属性
var runtimeProperty = obj.GetType() var runtimeProperty = obj.GetType().GetRuntimeProperty("Count");
.GetRuntimeProperty("Count");
// 反射获取 Count 属性值 // 反射获取 Count 属性值
if (runtimeProperty is not null if (runtimeProperty is not null && runtimeProperty.CanRead && runtimeProperty.PropertyType == typeof(int))
&& runtimeProperty.CanRead
&& runtimeProperty.PropertyType == typeof(int))
{ {
count = (int)runtimeProperty.GetValue(obj)!; count = (int)runtimeProperty.GetValue(obj)!;
return true; return true;

View File

@@ -38,7 +38,7 @@ public sealed class HttpContextForwardBuilder
/// <summary> /// <summary>
/// 忽略在转发时需要跳过的请求标头列表 /// 忽略在转发时需要跳过的请求标头列表
/// </summary> /// </summary>
internal static HashSet<string> _ignoreRequestHeaders = internal static readonly HashSet<string> _ignoreRequestHeaders =
[ [
Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding", Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding",
"Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges" "Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges"
@@ -356,8 +356,7 @@ public sealed class HttpContextForwardBuilder
if (multipartSection.AsFileSection() is not null) if (multipartSection.AsFileSection() is not null)
{ {
// 复制多部分表单内容文件节内容 // 复制多部分表单内容文件节内容
await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, httpRequestBuilder, await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, cancellationToken).ConfigureAwait(false);
cancellationToken).ConfigureAwait(false);
} }
else else
{ {
@@ -410,15 +409,11 @@ public sealed class HttpContextForwardBuilder
/// <param name="httpMultipartFormDataBuilder"> /// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" /> /// <see cref="HttpMultipartFormDataBuilder" />
/// </param> /// </param>
/// <param name="httpRequestBuilder">
/// <see cref="HttpRequestBuilder" />
/// </param>
/// <param name="cancellationToken"> /// <param name="cancellationToken">
/// <see cref="CancellationToken" /> /// <see cref="CancellationToken" />
/// </param> /// </param>
internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection, internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection,
HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, HttpRequestBuilder httpRequestBuilder, HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, CancellationToken cancellationToken)
CancellationToken cancellationToken)
{ {
// 初始化 MemoryStream 实例 // 初始化 MemoryStream 实例
var memoryStream = new MemoryStream(); var memoryStream = new MemoryStream();
@@ -433,10 +428,8 @@ public sealed class HttpContextForwardBuilder
var fileMultipartSection = multipartSection.AsFileSection()!; var fileMultipartSection = multipartSection.AsFileSection()!;
// 添加文件流 // 添加文件流
httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName); httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName,
disposeStreamOnRequestCompletion: true);
// 添加文件流到请求结束时需要释放的集合中
httpRequestBuilder.AddDisposable(memoryStream);
} }
/// <summary> /// <summary>

View File

@@ -124,12 +124,9 @@ public sealed class HttpMultipartFormDataBuilder
/// <see cref="HttpMultipartFormDataBuilder" /> /// <see cref="HttpMultipartFormDataBuilder" />
/// </returns> /// </returns>
/// <exception cref="JsonException"></exception> /// <exception cref="JsonException"></exception>
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) string? contentType = null)
{ {
// 空检查
ArgumentNullException.ThrowIfNull(rawJson);
// 检查是否配置表单名或不是字符串类型 // 检查是否配置表单名或不是字符串类型
if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString) if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString)
{ {
@@ -292,10 +289,8 @@ public sealed class HttpMultipartFormDataBuilder
// 从互联网 URL 地址中加载流 // 从互联网 URL 地址中加载流
var fileStream = Helpers.GetStreamFromRemote(url); var fileStream = Helpers.GetStreamFromRemote(url);
// 添加文件流到请求结束时需要释放的集合中 return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
_httpRequestBuilder.AddDisposable(fileStream); true);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
} }
/// <summary> /// <summary>
@@ -365,10 +360,8 @@ public sealed class HttpMultipartFormDataBuilder
// 读取文件流(没有 using // 读取文件流(没有 using
var fileStream = File.OpenRead(filePath); var fileStream = File.OpenRead(filePath);
// 添加文件流到请求结束时需要释放的集合中 return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
_httpRequestBuilder.AddDisposable(fileStream); true);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
} }
/// <summary> /// <summary>
@@ -407,10 +400,8 @@ public sealed class HttpMultipartFormDataBuilder
// 初始化带读写进度的文件流 // 初始化带读写进度的文件流
var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName); var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName);
// 添加文件流到请求结束时需要释放的集合中 return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding,
_httpRequestBuilder.AddDisposable(progressFileStream); true);
return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding);
} }
/// <summary> /// <summary>
@@ -500,11 +491,12 @@ public sealed class HttpMultipartFormDataBuilder
/// <param name="fileName">文件的名称</param> /// <param name="fileName">文件的名称</param>
/// <param name="contentType">内容类型</param> /// <param name="contentType">内容类型</param>
/// <param name="contentEncoding">内容编码</param> /// <param name="contentEncoding">内容编码</param>
/// <param name="disposeStreamOnRequestCompletion">是否在请求结束后自动释放流。默认值为:<c>false</c></param>
/// <returns> /// <returns>
/// <see cref="HttpMultipartFormDataBuilder" /> /// <see cref="HttpMultipartFormDataBuilder" />
/// </returns> /// </returns>
public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null, 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); ArgumentNullException.ThrowIfNull(stream);
@@ -529,6 +521,12 @@ public sealed class HttpMultipartFormDataBuilder
FileName = fileName FileName = fileName
}); });
// 是否在请求结束后自动释放流
if (disposeStreamOnRequestCompletion)
{
_httpRequestBuilder.AddDisposable(stream);
}
return this; return this;
} }
@@ -697,6 +695,20 @@ public sealed class HttpMultipartFormDataBuilder
return this; return this;
} }
/// <summary>
/// 设置是否移除默认的多部分内容的 <c>Content-Type</c>
/// </summary>
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public HttpMultipartFormDataBuilder SetOmitContentType(bool omit)
{
OmitContentType = omit;
return this;
}
/// <summary> /// <summary>
/// 构建 <see cref="MultipartFormDataContent" /> 实例 /// 构建 <see cref="MultipartFormDataContent" /> 实例
/// </summary> /// </summary>

View File

@@ -327,23 +327,20 @@ public sealed partial class HttpRequestBuilder
/// <param name="key">键</param> /// <param name="key">键</param>
/// <param name="value">值</param> /// <param name="value">值</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, CultureInfo? culture = null, public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, bool replace = false,
IEqualityComparer<string>? comparer = null, bool replace = false) CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer, replace); return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, replace, culture);
} }
/// <summary> /// <summary>
@@ -352,25 +349,22 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks> /// <remarks>支持多次调用。</remarks>
/// <param name="headers">请求标头集合</param> /// <param name="headers">请求标头集合</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithHeaders(IDictionary<string, object?> headers, bool escape = false, public HttpRequestBuilder WithHeaders(IDictionary<string, object?> headers, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false) bool replace = false, CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(headers); ArgumentNullException.ThrowIfNull(headers);
// 初始化请求标头 // 初始化请求标头
Headers ??= new Dictionary<string, List<string?>>(comparer); Headers ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
var objectHeaders = new Dictionary<string, List<object?>>(comparer); var objectHeaders = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加 // 存在则合并否则添加
objectHeaders.AddOrUpdate(Headers.ToDictionary(u => u.Key, object? (u) => u.Value), false); 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, Headers = objectHeaders.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u => kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(), u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
comparer); StringComparer.OrdinalIgnoreCase);
return this; return this;
} }
@@ -391,26 +385,23 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks> /// <remarks>支持多次调用。</remarks>
/// <param name="headerSource">请求标头源对象</param> /// <param name="headerSource">请求标头源对象</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, CultureInfo? culture = null, public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, bool replace = false,
IEqualityComparer<string>? comparer = null, bool replace = false) CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(headerSource); ArgumentNullException.ThrowIfNull(headerSource);
return WithHeaders( return WithHeaders(
headerSource.ObjectToDictionary()!.ToDictionary( headerSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture, u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, replace,
comparer, replace); culture);
} }
/// <summary> /// <summary>
@@ -474,6 +465,7 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder SetTimeout(TimeSpan timeout) public HttpRequestBuilder SetTimeout(TimeSpan timeout)
{ {
Timeout = timeout; Timeout = timeout;
TimeoutAction = null;
return this; return this;
} }
@@ -494,6 +486,43 @@ public sealed partial class HttpRequestBuilder
} }
Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds); Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
TimeoutAction = null;
return this;
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeout">超时时间</param>
/// <param name="onTimeout">超时发生时要执行的操作</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetTimeout(TimeSpan timeout, Action onTimeout)
{
// 空检查
ArgumentNullException.ThrowIfNull(onTimeout);
SetTimeout(timeout).TimeoutAction = onTimeout;
return this;
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeoutMilliseconds">超时时间(毫秒)</param>
/// <param name="onTimeout">超时发生时要执行的操作</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetTimeout(double timeoutMilliseconds, Action onTimeout)
{
// 空检查
ArgumentNullException.ThrowIfNull(onTimeout);
SetTimeout(timeoutMilliseconds).TimeoutAction = onTimeout;
return this; return this;
} }
@@ -570,26 +599,22 @@ public sealed partial class HttpRequestBuilder
/// <param name="key">键</param> /// <param name="key">键</param>
/// <param name="value">值</param> /// <param name="value">值</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, bool replace = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
bool ignoreNullValues = false)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer, return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, replace,
replace, ignoreNullValues); ignoreNullValues, culture);
} }
/// <summary> /// <summary>
@@ -598,27 +623,23 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks> /// <remarks>支持多次调用。</remarks>
/// <param name="parameters">查询参数集合</param> /// <param name="parameters">查询参数集合</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithQueryParameters(IDictionary<string, object?> parameters, bool escape = false, public HttpRequestBuilder WithQueryParameters(IDictionary<string, object?> parameters, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false, bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
bool ignoreNullValues = false)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(parameters);
// 初始化查询参数 // 初始化查询参数
QueryParameters ??= new Dictionary<string, List<string?>>(comparer); QueryParameters ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
var objectQueryParameters = new Dictionary<string, List<object?>>(comparer); var objectQueryParameters = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加 // 存在则合并否则添加
objectQueryParameters.AddOrUpdate(QueryParameters.ToDictionary(u => u.Key, object? (u) => u.Value), false); 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, QueryParameters = objectQueryParameters.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u => kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(), u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
comparer); StringComparer.OrdinalIgnoreCase);
return this; return this;
} }
@@ -641,20 +662,16 @@ public sealed partial class HttpRequestBuilder
/// <param name="parameterSource">查询参数集合</param> /// <param name="parameterSource">查询参数集合</param>
/// <param name="prefix">参数前缀。对于对象类型可生成如 <c>prefix.Name=furion</c> 与 <c>prefix.Age=30</c> 参数格式。</param> /// <param name="prefix">参数前缀。对于对象类型可生成如 <c>prefix.Name=furion</c> 与 <c>prefix.Age=30</c> 参数格式。</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false, public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false, bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
bool ignoreNullValues = false)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(parameterSource); ArgumentNullException.ThrowIfNull(parameterSource);
@@ -663,7 +680,7 @@ public sealed partial class HttpRequestBuilder
parameterSource.ObjectToDictionary()!.ToDictionary( parameterSource.ObjectToDictionary()!.ToDictionary(
u => u =>
$"{(string.IsNullOrWhiteSpace(prefix) ? null : $"{prefix}.")}{u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!}", $"{(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);
} }
/// <summary> /// <summary>
@@ -709,19 +726,16 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false, public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null) CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer); return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture);
} }
/// <summary> /// <summary>
@@ -733,26 +747,21 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters, public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters, bool escape = false,
bool escape = false, CultureInfo? culture = null)
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(parameters);
PathParameters ??= new Dictionary<string, string?>(comparer); PathParameters ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加 // 存在则更新否则添加
PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key, PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape), u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
comparer)); StringComparer.OrdinalIgnoreCase));
return this; return this;
} }
@@ -767,15 +776,11 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false, public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false,
CultureInfo? culture = null, CultureInfo? culture = null)
IEqualityComparer<string>? comparer = null)
{ {
// 检查是否设置了模板字符串前缀 // 检查是否设置了模板字符串前缀
if (string.IsNullOrWhiteSpace(prefix)) if (string.IsNullOrWhiteSpace(prefix))
@@ -786,7 +791,7 @@ public sealed partial class HttpRequestBuilder
return WithPathParameters( return WithPathParameters(
parameterSource.ObjectToDictionary()!.ToDictionary( parameterSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape,
culture, comparer); culture);
} }
ObjectPathParameters ??= new Dictionary<string, object?>(); ObjectPathParameters ??= new Dictionary<string, object?>();
@@ -823,19 +828,15 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null, public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null)
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer); return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture);
} }
/// <summary> /// <summary>
@@ -847,26 +848,21 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies, public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies, bool escape = false,
bool escape = false, CultureInfo? culture = null)
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(cookies); ArgumentNullException.ThrowIfNull(cookies);
Cookies ??= new Dictionary<string, string?>(comparer); Cookies ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加 // 存在则更新否则添加
Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key, Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape), u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
comparer)); StringComparer.OrdinalIgnoreCase));
return this; return this;
} }
@@ -880,15 +876,10 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, CultureInfo? culture = null)
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(cookieSource); ArgumentNullException.ThrowIfNull(cookieSource);
@@ -896,8 +887,7 @@ public sealed partial class HttpRequestBuilder
// 存在则更新否则添加 // 存在则更新否则添加
return WithCookies( return WithCookies(
cookieSource.ObjectToDictionary()!.ToDictionary( cookieSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture, u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture);
comparer);
} }
/// <summary> /// <summary>
@@ -1193,6 +1183,17 @@ public sealed partial class HttpRequestBuilder
return this; return this;
} }
/// <summary>
/// 设置身份验证凭据请求授权标头
/// </summary>
/// <param name="scheme">身份验证的方案</param>
/// <param name="parameter">身份验证的凭证</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder AddAuthentication(string scheme, string? parameter) =>
AddAuthentication(new AuthenticationHeaderValue(scheme, parameter));
/// <summary> /// <summary>
/// 设置身份验证凭据请求授权标头 /// 设置身份验证凭据请求授权标头
/// </summary> /// </summary>
@@ -1328,6 +1329,17 @@ public sealed partial class HttpRequestBuilder
ReleaseDisposables(); ReleaseDisposables();
} }
/// <summary>
/// 设置请求来源地址
/// </summary>
/// <remarks>设置此配置后,将在单次请求标头中添加 <c>Referer</c> 标头。</remarks>
/// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetReferer(string? referer) =>
WithHeader(HeaderNames.Referer, referer, replace: true);
/// <summary> /// <summary>
/// 设置模拟浏览器环境 /// 设置模拟浏览器环境
/// </summary> /// </summary>
@@ -1364,6 +1376,17 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder WithAnyStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) => public HttpRequestBuilder WithAnyStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
WithStatusCodeHandler(["*"], handler); WithStatusCodeHandler(["*"], handler);
/// <summary>
/// 添加请求成功200-299状态码处理程序
/// </summary>
/// <param name="handler">自定义处理程序</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder
WithSuccessStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
WithStatusCodeHandler("200-299", handler);
/// <summary> /// <summary>
/// 添加状态码处理程序 /// 添加状态码处理程序
/// </summary> /// </summary>
@@ -1590,6 +1613,107 @@ public sealed partial class HttpRequestBuilder
? null ? null
: new Uri(baseAddress, UriKind.RelativeOrAbsolute)); : new Uri(baseAddress, UriKind.RelativeOrAbsolute));
/// <summary>
/// 设置 HTTP 版本
/// </summary>
/// <param name="version">版本号</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetVersion(string? version) =>
SetVersion(string.IsNullOrWhiteSpace(version) ? null : new Version(version));
/// <summary>
/// 设置 HTTP 版本
/// </summary>
/// <param name="version">
/// <see cref="Version" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetVersion(Version? version)
{
Version = version;
return this;
}
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>抑制所有异常。重复调用仅最后一次调用生效。</remarks>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SuppressExceptions() => SuppressExceptions(true);
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>重复调用仅最后一次调用生效。</remarks>
/// <param name="enable">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SuppressExceptions(bool enable) => SuppressExceptions(enable ? [typeof(Exception)] : []);
/// <summary>
/// 设置是否移除默认的内容的 <c>Content-Type</c>
/// </summary>
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetOmitContentType(bool omit)
{
OmitContentType = omit;
return this;
}
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>重复调用仅最后一次调用生效。</remarks>
/// <param name="exceptionTypes">异常抑制类型集合</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
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;
}
/// <summary> /// <summary>
/// 释放可释放的对象集合 /// 释放可释放的对象集合
/// </summary> /// </summary>

View File

@@ -157,6 +157,11 @@ public sealed partial class HttpRequestBuilder
/// </summary> /// </summary>
public Uri? BaseAddress { get; private set; } public Uri? BaseAddress { get; private set; }
/// <summary>
/// HTTP 版本
/// </summary>
public Version? Version { get; private set; }
/// <summary> /// <summary>
/// <see cref="HttpClient" /> 实例提供器 /// <see cref="HttpClient" /> 实例提供器
/// </summary> /// </summary>
@@ -181,7 +186,7 @@ public sealed partial class HttpRequestBuilder
/// <summary> /// <summary>
/// 用于处理在设置 <see cref="HttpRequestMessage" /> 的请求消息的内容时的操作 /// 用于处理在设置 <see cref="HttpRequestMessage" /> 的请求消息的内容时的操作
/// </summary> /// </summary>
public Action<HttpContent?>? OnPreSetContent { get; private set; } public Action<HttpContent>? OnPreSetContent { get; private set; }
/// <summary> /// <summary>
/// 用于处理在发送 HTTP 请求之前的操作 /// 用于处理在发送 HTTP 请求之前的操作
@@ -201,7 +206,13 @@ public sealed partial class HttpRequestBuilder
/// <summary> /// <summary>
/// <inheritdoc cref="HttpMultipartFormDataBuilder" /> /// <inheritdoc cref="HttpMultipartFormDataBuilder" />
/// </summary> /// </summary>
internal HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; } public HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
/// <summary>
/// 是否移除默认的内容的 <c>Content-Type</c>
/// </summary>
/// <remarks>默认值为:<c>false</c>。</remarks>
public bool OmitContentType { get; private set; }
/// <summary> /// <summary>
/// 如果 HTTP 响应的 <c>IsSuccessStatusCode</c> 属性是 <c>false</c>,则引发异常。 /// 如果 HTTP 响应的 <c>IsSuccessStatusCode</c> 属性是 <c>false</c>,则引发异常。
@@ -273,4 +284,15 @@ public sealed partial class HttpRequestBuilder
get; get;
private set; private set;
} }
/// <summary>
/// 异常抑制类型集合
/// </summary>
/// <remarks>当配置了异常抑制类型集合后,框架将抑制(即不抛出)该集合中匹配的异常类型。</remarks>
internal HashSet<Type>? SuppressExceptionTypes { get; private set; }
/// <summary>
/// 超时发生时要执行的操作
/// </summary>
internal Action? TimeoutAction { get; private set; }
} }

View File

@@ -10,6 +10,9 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Reflection; using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ThingsGateway.HttpRemote; namespace ThingsGateway.HttpRemote;
@@ -614,4 +617,116 @@ public sealed partial class HttpRequestBuilder
/// </returns> /// </returns>
public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) => public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) =>
new(method, args); new(method, args);
/// <summary>
/// 从 JSON 中创建 <see cref="HttpRequestBuilder" /> 实例
/// </summary>
/// <param name="json">JSON 字符串</param>
/// <param name="configure">自定义配置委托</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static HttpRequestBuilder FromJson(string json, Action<HttpRequestBuilder>? 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<string?>());
// 处理可选字段
HandleJsonNode(jsonObject, "baseAddress", node => httpRequestBuilder.SetBaseAddress(node.GetValue<string>()));
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<double>()));
HandleJsonNode(jsonObject, "client", node => httpRequestBuilder.SetHttpClientName(node.GetValue<string?>()));
HandleJsonNode(jsonObject, "profiler", node => httpRequestBuilder.Profiler(node.GetValue<bool>()));
// 处理请求内容
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<string>()));
}
// 处理多部分表单
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;
}
/// <summary>
/// 处理 <see cref="JsonNode" />
/// </summary>
/// <param name="jsonObject">
/// <see cref="JsonObject" />
/// </param>
/// <param name="propertyName">属性名</param>
/// <param name="action">自定义操作</param>
internal static void HandleJsonNode(JsonObject jsonObject, string propertyName, Action<JsonNode> action)
{
// 空检查
ArgumentNullException.ThrowIfNull(jsonObject);
ArgumentNullException.ThrowIfNull(propertyName);
ArgumentNullException.ThrowIfNull(action);
if (jsonObject.TryGetPropertyValue(propertyName, out var node) && node is not null)
{
action(node);
}
}
} }

View File

@@ -76,6 +76,12 @@ public sealed partial class HttpRequestBuilder
// 初始化 HttpRequestMessage 实例 // 初始化 HttpRequestMessage 实例
var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri); var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri);
// 设置 HTTP 版本
if (Version is not null)
{
httpRequestMessage.Version = Version;
}
// 启用性能优化 // 启用性能优化
EnablePerformanceOptimization(httpRequestMessage); EnablePerformanceOptimization(httpRequestMessage);
@@ -160,18 +166,44 @@ public sealed partial class HttpRequestBuilder
/// </param> /// </param>
internal void AppendPathSegments(UriBuilder uriBuilder) 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 中的路径片段列表 // 解析 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)) var newPathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([])
.Select(u => u.TrimStart('/').TrimEnd('/'))); .Where(u => !string.IsNullOrWhiteSpace(u)).Select(u => u.TrimStart('/').TrimEnd('/')));
// 构建路径片段赋值给 UriBuilder 的 Path 属性 // 过滤需要移除的路径片段
uriBuilder.Path = '/' + string.Join('/', var filteredSegments = newPathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
// 过滤已标记为移除的路径片段 u => PathSegmentsToRemove?.Contains(u) == false).ToArray();
pathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
u => PathSegmentsToRemove?.TryGetValue(u, out _) == false)); // 构建最终路径
if (filteredSegments.Length != 0)
{
uriBuilder.Path = $"/{string.Join('/', filteredSegments)}";
// 恢复原路径的结尾斜杠(当存在路径片段时)
if (endsWithSlash)
{
uriBuilder.Path += "/";
}
}
// 没有路径片段时设置为根路径
else
{
uriBuilder.Path = "/";
}
} }
/// <summary> /// <summary>
@@ -182,6 +214,13 @@ public sealed partial class HttpRequestBuilder
/// </param> /// </param>
internal void AppendQueryParameters(UriBuilder uriBuilder) internal void AppendQueryParameters(UriBuilder uriBuilder)
{ {
// 空检查
if ((QueryParameters is null || QueryParameters.Count == 0) &&
(QueryParametersToRemove is null || QueryParametersToRemove.Count == 0))
{
return;
}
// 解析 URL 中的查询字符串为键值对列表 // 解析 URL 中的查询字符串为键值对列表
var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?'); var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?');
@@ -300,6 +339,16 @@ public sealed partial class HttpRequestBuilder
// 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中 // 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中
foreach (var (key, values) in 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); httpRequestMessage.Headers.TryAddWithoutValidation(key, values);
} }
} }
@@ -486,6 +535,18 @@ public sealed partial class HttpRequestBuilder
// 构建 HttpContent 实例 // 构建 HttpContent 实例
var httpContent = httpContentProcessorFactory.Build(RawContent, ContentType!, ContentEncoding, processors); 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); OnPreSetContent?.Invoke(httpContent);
@@ -513,6 +574,9 @@ public sealed partial class HttpRequestBuilder
{ {
httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE"); httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE");
} }
// 添加 HttpClient 实例的配置名称
httpRequestMessage.Options.AddOrUpdate(Constants.HTTP_CLIENT_NAME, HttpClientName ?? string.Empty);
} }
/// <summary> /// <summary>

View File

@@ -88,15 +88,26 @@ internal static class Constants
/// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks> /// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__"; internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__";
/// <summary>
/// HTTP 请求 <see cref="HttpClient" /> 实例的配置名称键
/// </summary>
/// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
internal const string HTTP_CLIENT_NAME = "__HTTP_CLIENT_NAME__";
/// <summary> /// <summary>
/// 浏览器的 <c>User-Agent</c> 标头值 /// 浏览器的 <c>User-Agent</c> 标头值
/// </summary> /// </summary>
internal const string USER_AGENT_OF_BROWSER = 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";
/// <summary> /// <summary>
/// 移动端浏览器的 <c>User-Agent</c> 标头值 /// 移动端浏览器的 <c>User-Agent</c> 标头值
/// </summary> /// </summary>
internal const string USER_AGENT_OF_MOBILE_BROWSER = 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";
/// <summary>
/// <c>Referer</c> 标头请求基地址模板
/// </summary>
internal const string REFERER_HEADER_BASE_ADDRESS_TEMPLATE = "{BASE_ADDRESS}";
} }

View File

@@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json;
namespace ThingsGateway.HttpRemote; namespace ThingsGateway.HttpRemote;
@@ -27,16 +28,45 @@ public class ObjectContentConverter : IHttpContentConverter
/// <inheritdoc /> /// <inheritdoc />
public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage, public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
httpResponseMessage.Content.ReadFromJsonAsync(resultType, httpResponseMessage.Content
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? .ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), cancellationToken)
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult(); .GetAwaiter().GetResult();
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, public virtual async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await httpResponseMessage.Content.ReadFromJsonAsync(resultType, await httpResponseMessage.Content.ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage),
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? cancellationToken).ConfigureAwait(false);
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
/// <summary>
/// 获取 JSON 序列化选项实例
/// </summary>
/// <param name="httpResponseMessage">
/// <see cref="HttpResponseMessage" />
/// </param>
/// <returns>
/// <see cref="JsonSerializerOptions" />
/// </returns>
protected virtual JsonSerializerOptions GetJsonSerializerOptions(HttpResponseMessage httpResponseMessage)
{
// 空检查
ArgumentNullException.ThrowIfNull(httpResponseMessage);
// 获取 HttpClient 实例的配置名称
if (httpResponseMessage.RequestMessage?.Options.TryGetValue(
new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME), out var httpClientName) != true)
{
httpClientName = string.Empty;
}
// 获取 HttpClientOptions 实例
var httpClientOptions = ServiceProvider?.GetService<IOptionsMonitor<HttpClientOptions>>()?.Get(httpClientName);
// 优先级:指定名称的 HttpClientOptions -> HttpRemoteOptions -> 默认值
return (httpClientOptions?.IsDefault != false ? null : httpClientOptions.JsonSerializerOptions) ??
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault;
}
} }
/// <summary> /// <summary>
@@ -48,14 +78,13 @@ public class ObjectContentConverter<TResult> : ObjectContentConverter, IHttpCont
/// <inheritdoc /> /// <inheritdoc />
public virtual TResult? Read(HttpResponseMessage httpResponseMessage, public virtual TResult? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
httpResponseMessage.Content.ReadFromJsonAsync<TResult>( httpResponseMessage.Content
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? .ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage), cancellationToken).GetAwaiter()
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult(); .GetResult();
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<TResult?> ReadAsync(HttpResponseMessage httpResponseMessage, public virtual async Task<TResult?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await httpResponseMessage.Content.ReadFromJsonAsync<TResult>( await httpResponseMessage.Content.ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage),
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? cancellationToken).ConfigureAwait(false);
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
} }

View File

@@ -18,10 +18,10 @@ public class VoidContentConverter : HttpContentConverterBase<VoidContent>
{ {
/// <inheritdoc /> /// <inheritdoc />
public override VoidContent? Read(HttpResponseMessage httpResponseMessage, public override VoidContent? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => default; CancellationToken cancellationToken = default) => null;
/// <inheritdoc /> /// <inheritdoc />
public override Task<VoidContent?> ReadAsync(HttpResponseMessage httpResponseMessage, public override Task<VoidContent?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
Task.FromResult<VoidContent?>(default); Task.FromResult<VoidContent?>(null);
} }

View File

@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 HTTP 版本特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class HttpVersionAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="HttpVersionAttribute" />
/// </summary>
/// <param name="version">HTTP 版本</param>
public HttpVersionAttribute(string? version) => Version = version;
/// <summary>
/// HTTP 版本
/// </summary>
public string? Version { get; set; }
}

View File

@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式请求来源地址特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class RefererAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="RefererAttribute" />
/// </summary>
/// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
public RefererAttribute(string? referer) => Referer = referer;
/// <summary>
/// 请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址
/// </summary>
public string? Referer { get; set; }
}

View File

@@ -0,0 +1,45 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式异常抑制特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class SuppressExceptionsAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <remarks>抑制所有异常。</remarks>
public SuppressExceptionsAttribute()
: this(true)
{
}
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <param name="enabled">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
public SuppressExceptionsAttribute(bool enabled) => Types = enabled ? [typeof(Exception)] : [];
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <param name="types">异常抑制类型集合</param>
public SuppressExceptionsAttribute(params Type[] types) => Types = types;
/// <summary>
/// 异常抑制类型集合
/// </summary>
public Type[] Types { get; set; }
}

View File

@@ -44,8 +44,11 @@ public sealed class HttpDeclarativeBuilder
new(typeof(QueryDeclarativeExtractor), new QueryDeclarativeExtractor()), new(typeof(QueryDeclarativeExtractor), new QueryDeclarativeExtractor()),
new(typeof(PathDeclarativeExtractor), new PathDeclarativeExtractor()), new(typeof(PathDeclarativeExtractor), new PathDeclarativeExtractor()),
new(typeof(CookieDeclarativeExtractor), new CookieDeclarativeExtractor()), new(typeof(CookieDeclarativeExtractor), new CookieDeclarativeExtractor()),
new(typeof(RefererDeclarativeExtractor), new RefererDeclarativeExtractor()),
new(typeof(HeaderDeclarativeExtractor), new HeaderDeclarativeExtractor()), new(typeof(HeaderDeclarativeExtractor), new HeaderDeclarativeExtractor()),
new(typeof(PropertyDeclarativeExtractor), new PropertyDeclarativeExtractor()), new(typeof(PropertyDeclarativeExtractor), new PropertyDeclarativeExtractor()),
new(typeof(HttpVersionDeclarativeExtractor), new HttpVersionDeclarativeExtractor()),
new(typeof(SuppressExceptionsDeclarativeExtractor), new SuppressExceptionsDeclarativeExtractor()),
new(typeof(BodyDeclarativeExtractor), new BodyDeclarativeExtractor()) new(typeof(BodyDeclarativeExtractor), new BodyDeclarativeExtractor())
]); ]);

View File

@@ -45,7 +45,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
if (headerAttribute.HasSetValue) if (headerAttribute.HasSetValue)
{ {
httpRequestBuilder.WithHeader(headerName, headerAttribute.Value, headerAttribute.Escape, httpRequestBuilder.WithHeader(headerName, headerAttribute.Value, headerAttribute.Escape,
replace: headerAttribute.Replace); headerAttribute.Replace);
} }
// 移除请求标头 // 移除请求标头
else else
@@ -91,7 +91,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection()) if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection())
{ {
httpRequestBuilder.WithHeader(parameterName, value ?? headerAttribute.Value, httpRequestBuilder.WithHeader(parameterName, value ?? headerAttribute.Value,
headerAttribute.Escape, replace: headerAttribute.Replace); headerAttribute.Escape, headerAttribute.Replace);
continue; continue;
} }
@@ -99,7 +99,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
// 空检查 // 空检查
if (value is not null) if (value is not null)
{ {
httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, replace: headerAttribute.Replace); httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, headerAttribute.Replace);
} }
} }
} }

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="HttpVersionAttribute" /> 特性提取器
/// </summary>
internal sealed class HttpVersionDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [HttpVersion] 特性
if (!context.IsMethodDefined<HttpVersionAttribute>(out var versionAttribute, true))
{
return;
}
// 设置 HTTP 版本
httpRequestBuilder.SetVersion(versionAttribute.Version);
}
}

View File

@@ -45,7 +45,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor
if (queryAttribute.HasSetValue) if (queryAttribute.HasSetValue)
{ {
httpRequestBuilder.WithQueryParameter(queryName, queryAttribute.Value, queryAttribute.Escape, httpRequestBuilder.WithQueryParameter(queryName, queryAttribute.Value, queryAttribute.Escape,
replace: queryAttribute.Replace, ignoreNullValues: queryAttribute.IgnoreNullValues); queryAttribute.Replace, queryAttribute.IgnoreNullValues);
} }
// 移除查询参数 // 移除查询参数
else else
@@ -91,8 +91,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor
if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection()) if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection())
{ {
httpRequestBuilder.WithQueryParameter(parameterName, value ?? queryAttribute.Value, httpRequestBuilder.WithQueryParameter(parameterName, value ?? queryAttribute.Value,
queryAttribute.Escape, replace: queryAttribute.Replace, queryAttribute.Escape, queryAttribute.Replace, queryAttribute.IgnoreNullValues);
ignoreNullValues: queryAttribute.IgnoreNullValues);
continue; continue;
} }
@@ -101,7 +100,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor
if (value is not null) if (value is not null)
{ {
httpRequestBuilder.WithQueryParameters(value, queryAttribute.Prefix, queryAttribute.Escape, httpRequestBuilder.WithQueryParameters(value, queryAttribute.Prefix, queryAttribute.Escape,
replace: queryAttribute.Replace, ignoreNullValues: queryAttribute.IgnoreNullValues); queryAttribute.Replace, queryAttribute.IgnoreNullValues);
} }
} }
} }

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="RefererAttribute" /> 特性提取器
/// </summary>
internal sealed class RefererDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [Referer] 特性
if (!context.IsMethodDefined<RefererAttribute>(out var refererAttribute, true))
{
return;
}
// 设置请求来源地址
httpRequestBuilder.SetReferer(refererAttribute.Referer);
}
}

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="SuppressExceptionsAttribute" /> 特性提取器
/// </summary>
internal sealed class SuppressExceptionsDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [SuppressExceptions] 特性
if (!context.IsMethodDefined<SuppressExceptionsAttribute>(out var suppressExceptionsAttribute, true))
{
return;
}
// 设置异常抑制
httpRequestBuilder.SuppressExceptions(suppressExceptionsAttribute.Types);
}
}

View File

@@ -25,7 +25,7 @@ public sealed class HttpDeclarativeExtractorContext
/// 冻结参数类型集合 /// 冻结参数类型集合
/// </summary> /// </summary>
/// <remarks>此类参数类型不应作为外部提取对象。</remarks> /// <remarks>此类参数类型不应作为外部提取对象。</remarks>
internal static Type[] _frozenParameterTypes = internal static readonly Type[] _frozenParameterTypes =
[ [
typeof(Action<HttpRequestBuilder>), typeof(Action<HttpMultipartFormDataBuilder>), typeof(HttpCompletionOption), typeof(Action<HttpRequestBuilder>), typeof(Action<HttpMultipartFormDataBuilder>), typeof(HttpCompletionOption),
typeof(CancellationToken) typeof(CancellationToken)

View File

@@ -42,7 +42,7 @@ public static partial class HttpContextExtensions
/// </item> /// </item>
/// </list> /// </list>
/// </remarks> /// </remarks>
internal static HashSet<string> _ignoreResponseHeaders = internal static readonly HashSet<string> _ignoreResponseHeaders =
[ [
"Content-Type", "Connection", "Transfer-Encoding", "Keep-Alive", "Upgrade", "Proxy-Connection" "Content-Type", "Connection", "Transfer-Encoding", "Keep-Alive", "Upgrade", "Proxy-Connection"
]; ];
@@ -64,7 +64,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, string? requestUri = null, public static HttpResponseMessage? Forward(this HttpContext? httpContext, string? requestUri = null,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -90,7 +90,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, HttpMethod httpMethod, public static HttpResponseMessage? Forward(this HttpContext? httpContext, HttpMethod httpMethod,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null, string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -115,7 +115,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, Uri? requestUri = null, public static HttpResponseMessage? Forward(this HttpContext? httpContext, Uri? requestUri = null,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -140,7 +140,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, HttpMethod httpMethod, public static HttpResponseMessage? Forward(this HttpContext? httpContext, HttpMethod httpMethod,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) HttpContextForwardOptions? forwardOptions = null)
@@ -180,7 +180,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, string? requestUri = null, public static Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, string? requestUri = null,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -206,7 +206,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, public static Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null, string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -231,7 +231,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null, public static Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -256,7 +256,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
public static async Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod, public static async Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) HttpContextForwardOptions? forwardOptions = null)
@@ -297,7 +297,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, string? requestUri = null, public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, string? requestUri = null,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -324,7 +324,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod, public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null, string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -350,7 +350,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, Uri? requestUri = null, public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, Uri? requestUri = null,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -376,7 +376,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod, public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) HttpContextForwardOptions? forwardOptions = null)
@@ -393,7 +393,7 @@ public static partial class HttpContextExtensions
var result = httpRemoteService.Send<TResult>(httpRequestBuilder, completionOption, httpContext.RequestAborted); var result = httpRemoteService.Send<TResult>(httpRequestBuilder, completionOption, httpContext.RequestAborted);
// 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中 // 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中
ForwardResponseMessage(httpContext, result.ResponseMessage, httpContextForwardBuilder.ForwardOptions); ForwardResponseMessage(httpContext, result?.ResponseMessage, httpContextForwardBuilder.ForwardOptions);
return result; return result;
} }
@@ -416,7 +416,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext, public static Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null, string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -443,7 +443,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext, public static Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
HttpMethod httpMethod, string? requestUri = null, Action<HttpRequestBuilder>? configure = null, HttpMethod httpMethod, string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -469,7 +469,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext, public static Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) => HttpContextForwardOptions? forwardOptions = null) =>
@@ -495,7 +495,7 @@ public static partial class HttpContextExtensions
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
public static async Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext, public static async Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
HttpMethod httpMethod, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null, HttpMethod httpMethod, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) HttpContextForwardOptions? forwardOptions = null)
@@ -513,7 +513,7 @@ public static partial class HttpContextExtensions
httpContext.RequestAborted).ConfigureAwait(false); httpContext.RequestAborted).ConfigureAwait(false);
// 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中 // 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中
ForwardResponseMessage(httpContext, result.ResponseMessage, httpContextForwardBuilder.ForwardOptions); ForwardResponseMessage(httpContext, result?.ResponseMessage, httpContextForwardBuilder.ForwardOptions);
return result; return result;
} }
@@ -605,14 +605,22 @@ public static partial class HttpContextExtensions
/// <param name="forwardOptions"> /// <param name="forwardOptions">
/// <see cref="HttpContextForwardOptions" /> /// <see cref="HttpContextForwardOptions" />
/// </param> /// </param>
internal static void ForwardResponseMessage(HttpContext httpContext, HttpResponseMessage httpResponseMessage, internal static void ForwardResponseMessage(HttpContext httpContext, HttpResponseMessage? httpResponseMessage,
HttpContextForwardOptions forwardOptions) HttpContextForwardOptions forwardOptions)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(httpResponseMessage);
ArgumentNullException.ThrowIfNull(forwardOptions); ArgumentNullException.ThrowIfNull(forwardOptions);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 获取 HttpResponse 实例 // 获取 HttpResponse 实例
var httpResponse = httpContext.Response; var httpResponse = httpContext.Response;

View File

@@ -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;
/// <summary>
/// <see cref="HttpMultipartFormDataBuilder" /> 拓展类
/// </summary>
public static class HttpMultipartFormDataBuilderExtensions
{
/// <summary>
/// 添加文件
/// </summary>
/// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" />
/// </param>
/// <param name="formFile">
/// <see cref="IFormFile" />
/// </param>
/// <param name="name">表单名称</param>
/// <param name="fileName">文件的名称</param>
/// <param name="contentType">内容类型</param>
/// <param name="contentEncoding">内容编码</param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
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);
}
/// <summary>
/// 添加多个文件
/// </summary>
/// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" />
/// </param>
/// <param name="formFiles">
/// <see cref="IFormFileCollection" />
/// </param>
/// <param name="name">表单名称</param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public static HttpMultipartFormDataBuilder AddFiles(this HttpMultipartFormDataBuilder httpMultipartFormDataBuilder,
IEnumerable<IFormFile> formFiles, string? name = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(formFiles);
// 逐条添加文件
foreach (var formFile in formFiles)
{
httpMultipartFormDataBuilder.AddFile(formFile, name ?? formFile.Name);
}
return httpMultipartFormDataBuilder;
}
}

View File

@@ -16,6 +16,7 @@ using Microsoft.Net.Http.Headers;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions; using ThingsGateway.Extensions;
using ThingsGateway.Utilities; using ThingsGateway.Utilities;
@@ -27,7 +28,7 @@ namespace ThingsGateway.HttpRemote.Extensions;
/// <summary> /// <summary>
/// HTTP 远程服务拓展类 /// HTTP 远程服务拓展类
/// </summary> /// </summary>
public static class HttpRemoteExtensions public static partial class HttpRemoteExtensions
{ {
/// <summary> /// <summary>
/// 添加 HTTP 远程请求分析工具处理委托 /// 添加 HTTP 远程请求分析工具处理委托
@@ -70,6 +71,56 @@ public static class HttpRemoteExtensions
builder.AddProfilerDelegatingHandler(() => builder.AddProfilerDelegatingHandler(() =>
disableInProduction && GetHostEnvironmentName(builder.Services)?.ToLower() == "production"); disableInProduction && GetHostEnvironmentName(builder.Services)?.ToLower() == "production");
/// <summary>
/// 配置 <see cref="HttpClient" /> 额外选项
/// </summary>
/// <param name="builder">
/// <see cref="IHttpClientBuilder" />
/// </param>
/// <param name="configure">自定义配置选项</param>
/// <returns>
/// <see cref="IHttpClientBuilder" />
/// </returns>
public static IHttpClientBuilder ConfigureOptions(this IHttpClientBuilder builder,
Action<HttpClientOptions> configure)
{
// 空检查
ArgumentNullException.ThrowIfNull(configure);
builder.Services.AddOptions<HttpClientOptions>(builder.Name).Configure(options =>
{
options.IsDefault = false;
configure.Invoke(options);
});
return builder;
}
/// <summary>
/// 配置 <see cref="HttpClient" /> 额外选项
/// </summary>
/// <param name="builder">
/// <see cref="IHttpClientBuilder" />
/// </param>
/// <param name="configure">自定义配置选项</param>
/// <returns>
/// <see cref="IHttpClientBuilder" />
/// </returns>
public static IHttpClientBuilder ConfigureOptions(this IHttpClientBuilder builder,
Action<HttpClientOptions, IServiceProvider> configure)
{
// 空检查
ArgumentNullException.ThrowIfNull(configure);
builder.Services.AddOptions<HttpClientOptions>(builder.Name).Configure<IServiceProvider>((options, provider) =>
{
options.IsDefault = false;
configure.Invoke(options, provider);
});
return builder;
}
/// <summary> /// <summary>
/// 为 <see cref="HttpClient" /> 启用性能优化 /// 为 <see cref="HttpClient" /> 启用性能优化
/// </summary> /// </summary>
@@ -160,17 +211,26 @@ public static class HttpRemoteExtensions
? [new KeyValuePair<string, IEnumerable<string>>("Declarative", [methodSignature])] ? [new KeyValuePair<string, IEnumerable<string>>("Declarative", [methodSignature])]
: null; : null;
// 格式化 HttpClient 实例的配置条目
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? httpClientKeyValues =
httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME),
out var httpClientName)
? [new KeyValuePair<string, IEnumerable<string>>("HttpClient Name", [httpClientName])]
: null;
// 格式化常规条目 // 格式化常规条目
var generalEntry = StringUtility.FormatKeyValuesSummary(new[] var generalEntry = StringUtility.FormatKeyValuesSummary(new[]
{ {
new KeyValuePair<string, IEnumerable<string>>("Request URL", new KeyValuePair<string, IEnumerable<string>>("Request URL",
[httpRequestMessage.RequestUri?.OriginalString!]), [httpRequestMessage.RequestUri?.OriginalString!]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Method", [httpRequestMessage.Method.ToString()]), new KeyValuePair<string, IEnumerable<string>>("HTTP Method", [httpRequestMessage.Method.ToString()]),
new KeyValuePair<string, IEnumerable<string>>("Status Code", new KeyValuePair<string, IEnumerable<string>>("Status Code",
[$"{(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}"]), [$"{(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}"]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Content", new KeyValuePair<string, IEnumerable<string>>("HTTP Version", [httpResponseMessage.Version.ToString()]),
[$"{httpContent?.GetType().Name}"]) new KeyValuePair<string, IEnumerable<string>>("HTTP Content",
}.ConcatIgnoreNull(declarativeKeyValues).ConcatIgnoreNull(generalCustomKeyValues), generalSummary); [$"{httpContent?.GetType().Name}"])
}.ConcatIgnoreNull(httpClientKeyValues).ConcatIgnoreNull(declarativeKeyValues)
.ConcatIgnoreNull(generalCustomKeyValues), generalSummary);
// 格式化响应条目 // 格式化响应条目
var responseEntry = httpResponseMessage.ProfilerHeaders(responseSummary); var responseEntry = httpResponseMessage.ProfilerHeaders(responseSummary);
@@ -203,19 +263,43 @@ public static class HttpRemoteExtensions
// 默认只读取 5KB 的内容 // 默认只读取 5KB 的内容
const int maxBytesToDisplay = 5120; const int maxBytesToDisplay = 5120;
// 读取内容为字节数组 /*
* 读取内容为字节数组
*
* 由于 HttpContent 的流设计为单次读取(即流内容在首次读取后会被消耗,无法重复读取),
* 当前实现(即使用 ReadAsByteArrayAsync(cancellationToken))中对于较大内容会一次性加载至内存,
* 这可能导致性能问题(如内存占用过高或响应延迟),不过目前尚未找到更优的解决方案。
*
* 强烈建议在生产环境中禁用或关闭此类一次性读取操作,尤其是对于高并发或大流量场景,
* 以避免因内存溢出OOM或线程阻塞导致的服务不可用风险。
*/
var buffer = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); var buffer = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
var total = buffer.Length;
// 计算要显示的部分 // 计算要显示的部分
var bytesToShow = Math.Min(buffer.Length, maxBytesToDisplay); var bytesToShow = Math.Min(total, maxBytesToDisplay);
var partialContent = Encoding.UTF8.GetString(buffer, 0, bytesToShow);
// 注册 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( return StringUtility.FormatKeyValuesSummary(
[new KeyValuePair<string, IEnumerable<string>>(string.Empty, [bodyString])], [new KeyValuePair<string, IEnumerable<string>>(string.Empty, [bodyString])],
$"{summary} ({httpContent.GetType().Name})"); $"{summary} ({httpContent.GetType().Name}, total: {total} bytes)");
} }
/// <summary> /// <summary>
@@ -359,4 +443,13 @@ public static class HttpRemoteExtensions
? null ? null
: Convert.ToString(hostEnvironment.GetType().GetProperty("EnvironmentName")?.GetValue(hostEnvironment)); : Convert.ToString(hostEnvironment.GetType().GetProperty("EnvironmentName")?.GetValue(hostEnvironment));
} }
/// <summary>
/// Unicode 转义正则表达式
/// </summary>
/// <returns>
/// <see cref="Regex" />
/// </returns>
[GeneratedRegex(@"\\u([0-9a-fA-F]{4})")]
private static partial Regex UnicodeEscapeRegex();
} }

View File

@@ -52,27 +52,44 @@ internal sealed class HttpContentConverterFactory : IHttpContentConverterFactory
public IServiceProvider ServiceProvider { get; } public IServiceProvider ServiceProvider { get; }
/// <inheritdoc /> /// <inheritdoc />
public TResult? Read<TResult>(HttpResponseMessage httpResponseMessage, IHttpContentConverter[]? converters = null, public TResult? Read<TResult>(HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
GetConverter<TResult>(converters).Read(httpResponseMessage, cancellationToken); httpResponseMessage is null
? default
: GetConverter<TResult>(converters).Read(httpResponseMessage, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public object? Read(Type resultType, HttpResponseMessage httpResponseMessage, public object? Read(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default) =>
CancellationToken cancellationToken = default) => httpResponseMessage is null
GetConverter(resultType, converters).Read(resultType, httpResponseMessage, cancellationToken); ? null
: GetConverter(resultType, converters).Read(resultType, httpResponseMessage, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public async Task<TResult?> ReadAsync<TResult>(HttpResponseMessage httpResponseMessage, public async Task<TResult?> ReadAsync<TResult>(HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default) => {
await GetConverter<TResult>(converters).ReadAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); // 空检查
if (httpResponseMessage is null)
{
return default;
}
return await GetConverter<TResult>(converters).ReadAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, public async Task<object?> ReadAsync(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default) => {
await GetConverter(resultType, converters).ReadAsync(resultType, httpResponseMessage, cancellationToken).ConfigureAwait(false); // 空检查
if (httpResponseMessage is null)
{
return null;
}
return await GetConverter(resultType, converters).ReadAsync(resultType, httpResponseMessage, cancellationToken).ConfigureAwait(false);
}
/// <summary> /// <summary>
/// 获取 <see cref="IHttpContentConverter{TResult}" /> 实例 /// 获取 <see cref="IHttpContentConverter{TResult}" /> 实例

View File

@@ -37,7 +37,7 @@ public interface IHttpContentConverterFactory
/// <returns> /// <returns>
/// <typeparamref name="TResult" /> /// <typeparamref name="TResult" />
/// </returns> /// </returns>
TResult? Read<TResult>(HttpResponseMessage httpResponseMessage, TResult? Read<TResult>(HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -54,7 +54,7 @@ public interface IHttpContentConverterFactory
/// <returns> /// <returns>
/// <see cref="object" /> /// <see cref="object" />
/// </returns> /// </returns>
object? Read(Type resultType, HttpResponseMessage httpResponseMessage, object? Read(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -73,7 +73,7 @@ public interface IHttpContentConverterFactory
/// <returns> /// <returns>
/// <typeparamref name="TResult" /> /// <typeparamref name="TResult" />
/// </returns> /// </returns>
Task<TResult?> ReadAsync<TResult>(HttpResponseMessage httpResponseMessage, Task<TResult?> ReadAsync<TResult>(HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -90,6 +90,6 @@ public interface IHttpContentConverterFactory
/// <returns> /// <returns>
/// <see cref="object" /> /// <see cref="object" />
/// </returns> /// </returns>
Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, Task<object?> ReadAsync(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default); IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
} }

View File

@@ -115,6 +115,15 @@ internal sealed class FileDownloadManager
var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead, var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead,
cancellationToken); cancellationToken);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 根据文件是否存在及配置的行为来决定是否应继续进行文件下载 // 根据文件是否存在及配置的行为来决定是否应继续进行文件下载
if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath)) if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath))
{ {
@@ -228,6 +237,15 @@ internal sealed class FileDownloadManager
var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder,
HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); 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)) if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath))
{ {
@@ -246,7 +264,7 @@ internal sealed class FileDownloadManager
bufferSize, true); bufferSize, true);
// 获取 HTTP 响应体中的内容流 // 获取 HTTP 响应体中的内容流
using var contentStream = (await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// 循环读取数据直到取消请求或读取完毕 // 循环读取数据直到取消请求或读取完毕
int numBytesRead; int numBytesRead;
@@ -485,6 +503,9 @@ internal sealed class FileDownloadManager
/// </returns> /// </returns>
internal string GetFileName(HttpResponseMessage httpResponseMessage) internal string GetFileName(HttpResponseMessage httpResponseMessage)
{ {
// 空检查
ArgumentNullException.ThrowIfNull(httpResponseMessage);
// 获取文件下载保存的文件的名称 // 获取文件下载保存的文件的名称
var fileName = Path.GetFileName(_httpFileDownloadBuilder.DestinationPath); var fileName = Path.GetFileName(_httpFileDownloadBuilder.DestinationPath);

View File

@@ -88,7 +88,7 @@ internal sealed class FileUploadManager
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
/// <exception cref="NotImplementedException"></exception> /// <exception cref="NotImplementedException"></exception>
internal HttpResponseMessage Start(CancellationToken cancellationToken = default) internal HttpResponseMessage? Start(CancellationToken cancellationToken = default)
{ {
// 创建进度报告任务取消标识 // 创建进度报告任务取消标识
using var progressCancellationTokenSource = new CancellationTokenSource(); using var progressCancellationTokenSource = new CancellationTokenSource();
@@ -102,7 +102,7 @@ internal sealed class FileUploadManager
// 初始化 Stopwatch 实例并开启计时操作 // 初始化 Stopwatch 实例并开启计时操作
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
HttpResponseMessage httpResponseMessage; HttpResponseMessage? httpResponseMessage;
try try
{ {
@@ -147,7 +147,7 @@ internal sealed class FileUploadManager
/// <returns> /// <returns>
/// <see cref="Task{TResult}" /> /// <see cref="Task{TResult}" />
/// </returns> /// </returns>
internal async Task<HttpResponseMessage> StartAsync(CancellationToken cancellationToken = default) internal async Task<HttpResponseMessage?> StartAsync(CancellationToken cancellationToken = default)
{ {
// 创建进度报告任务取消标识 // 创建进度报告任务取消标识
using var progressCancellationTokenSource = new CancellationTokenSource(); using var progressCancellationTokenSource = new CancellationTokenSource();
@@ -161,7 +161,7 @@ internal sealed class FileUploadManager
// 初始化 Stopwatch 实例并开启计时操作 // 初始化 Stopwatch 实例并开启计时操作
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
HttpResponseMessage httpResponseMessage; HttpResponseMessage? httpResponseMessage;
try try
{ {

View File

@@ -97,6 +97,15 @@ internal sealed class LongPollingManager
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, cancellationToken); 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); dataChannel.Writer.TryWrite(httpResponseMessage);
@@ -167,6 +176,15 @@ internal sealed class LongPollingManager
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, cancellationToken).ConfigureAwait(false); 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); await dataChannel.Writer.WriteAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false);

View File

@@ -109,6 +109,15 @@ internal sealed class ServerSentEventsManager
var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead, var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead,
cancellationToken); cancellationToken);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 获取 HTTP 响应体中的内容流 // 获取 HTTP 响应体中的内容流
using var contentStream = httpResponseMessage.Content.ReadAsStream(cancellationToken); using var contentStream = httpResponseMessage.Content.ReadAsStream(cancellationToken);
@@ -203,9 +212,17 @@ internal sealed class ServerSentEventsManager
var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder,
HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); 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 实例 // 初始化 StreamReader 实例
using var streamReader = new StreamReader(contentStream, Encoding.UTF8); using var streamReader = new StreamReader(contentStream, Encoding.UTF8);
@@ -382,10 +399,10 @@ internal sealed class ServerSentEventsManager
? retryInterval ? retryInterval
: _httpServerSentEventsBuilder.DefaultRetryInterval; : _httpServerSentEventsBuilder.DefaultRetryInterval;
break; break;
// 所有其他的字段名都会被忽略 // 其他的字段名存储在 CustomFields 属性中
default: default:
// 保持数据不变 serverSentEventsData.AddCustomField(key, value);
return true; break;
} }
return true; return true;

View File

@@ -143,6 +143,15 @@ internal sealed class StressTestHarnessManager
var httpResponseMessage = var httpResponseMessage =
await _httpRemoteService.SendAsync(RequestBuilder, completionOption, cancellationToken).ConfigureAwait(false); 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) if (httpResponseMessage.IsSuccessStatusCode)
{ {

View File

@@ -36,7 +36,7 @@ public sealed class HttpRemoteAnalyzer
/// <summary> /// <summary>
/// 分析数据 /// 分析数据
/// </summary> /// </summary>
public string Data => _cachedData ??= _dataBuffer.ToString(); public string Data => _cachedData ??= _dataBuffer.ToString().TrimEnd(Environment.NewLine.ToCharArray());
/// <summary> /// <summary>
/// 追加分析数据 /// 追加分析数据
@@ -44,7 +44,7 @@ public sealed class HttpRemoteAnalyzer
/// <param name="value">分析数据</param> /// <param name="value">分析数据</param>
internal void AppendData(string? value) internal void AppendData(string? value)
{ {
_dataBuffer.Append(value); _dataBuffer.AppendLine(value);
_cachedData = null; _cachedData = null;
} }

View File

@@ -0,0 +1,186 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
namespace ThingsGateway.HttpRemote;
/// <summary>
/// 提供静态访问 <see cref="IHttpRemoteService" /> 服务的方式
/// </summary>
/// <remarks>支持服务的延迟初始化、配置更新以及资源释放。</remarks>
#pragma warning disable CA1513
public static class HttpRemoteClient
{
/// <inheritdoc cref="IServiceProvider" />
internal static IServiceProvider? _serviceProvider;
/// <summary>
/// 延迟加载的 <see cref="IHttpRemoteService" /> 实例
/// </summary>
internal static Lazy<IHttpRemoteService> _lazyService;
/// <summary>
/// 并发锁对象
/// </summary>
internal static readonly object _lock = new();
/// <summary>
/// 标记服务是否已释放
/// </summary>
internal static bool _isDisposed;
/// <summary>
/// 自定义服务注册逻辑的委托
/// </summary>
internal static Action<IServiceCollection> _configure = services => services.AddHttpRemote();
/// <summary>
/// <inheritdoc cref="HttpRemoteClient" />
/// </summary>
static HttpRemoteClient() => _lazyService = new Lazy<IHttpRemoteService>(CreateService);
/// <summary>
/// 获取当前配置下的 <see cref="IHttpRemoteService" /> 实例
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
public static IHttpRemoteService Service
{
get
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(HttpRemoteClient));
}
return _lazyService.Value;
}
}
/// <summary>
/// 自定义服务注册逻辑
/// </summary>
public static void Configure(Action<IServiceCollection> configure)
{
// 空检查
ArgumentNullException.ThrowIfNull(configure);
lock (_lock)
{
_configure = configure;
// 重新初始化服务
Reinitialize();
}
}
/// <summary>
/// 释放服务提供器及相关资源
/// </summary>
/// <remarks>通常在应用程序关闭或不再需要 HTTP 远程请求服务时调用。</remarks>
public static void Dispose()
{
lock (_lock)
{
if (_isDisposed)
{
return;
}
// 释放服务提供器
ReleaseServiceProvider();
_isDisposed = true;
}
}
/// <summary>
/// 创建 <see cref="IHttpRemoteService" /> 实例
/// </summary>
/// <returns>
/// <see cref="IHttpRemoteService" />
/// </returns>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="InvalidOperationException"></exception>
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<IHttpRemoteService>();
return service;
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to initialize IHttpRemoteService.", ex);
}
}
}
/// <summary>
/// 使用最新的 <see cref="Configure" /> 配置重新初始化服务
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
internal static void Reinitialize()
{
lock (_lock)
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(HttpRemoteClient));
}
// 释放当前的服务提供器
ReleaseServiceProvider();
// 重新创建延迟加载实例
_lazyService = new Lazy<IHttpRemoteService>(CreateService, LazyThreadSafetyMode.ExecutionAndPublication);
}
}
/// <summary>
/// 释放服务提供器
/// </summary>
internal static void ReleaseServiceProvider()
{
// 如果服务提供器支持释放资源,则执行释放操作
if (_serviceProvider is IDisposable disposable)
{
disposable.Dispose();
}
_serviceProvider = null;
}
}
#pragma warning restore CA1513

View File

@@ -108,6 +108,80 @@ public sealed class HttpRemoteResult<TResult>
/// </summary> /// </summary>
public HttpContentHeaders ContentHeaders { get; private set; } = null!; public HttpContentHeaders ContentHeaders { get; private set; } = null!;
/// <summary>
/// HTTP 版本
/// </summary>
public Version Version { get; private set; } = null!;
/// <summary>
/// <see cref="HttpClient" /> 实例的配置名称
/// </summary>
public string? HttpClientName { get; private set; }
// /// <summary>
// /// 解构函数(至少包含两个 out 参数!!!)
// /// </summary>
// /// <param name="result">
// /// <typeparamref name="TResult" />
// /// </param>
// public void Deconstruct(out TResult? result)
// {
// result = Result;
// }
/// <summary>
/// 解构函数
/// </summary>
/// <param name="result">
/// <typeparamref name="TResult" />
/// </param>
/// <param name="httpResponseMessage">
/// <inheritdoc cref="HttpResponseMessage" />
/// </param>
public void Deconstruct(out TResult? result, out HttpResponseMessage httpResponseMessage)
{
result = Result;
httpResponseMessage = ResponseMessage;
}
/// <summary>
/// 解构函数
/// </summary>
/// <param name="result">
/// <typeparamref name="TResult" />
/// </param>
/// <param name="httpResponseMessage">
/// <inheritdoc cref="HttpResponseMessage" />
/// </param>
/// <param name="isSuccessStatusCode">是否请求成功</param>
public void Deconstruct(out TResult? result, out HttpResponseMessage httpResponseMessage,
out bool isSuccessStatusCode)
{
result = Result;
httpResponseMessage = ResponseMessage;
isSuccessStatusCode = IsSuccessStatusCode;
}
/// <summary>
/// 解构函数
/// </summary>
/// <param name="result">
/// <typeparamref name="TResult" />
/// </param>
/// <param name="httpResponseMessage">
/// <inheritdoc cref="HttpResponseMessage" />
/// </param>
/// <param name="isSuccessStatusCode">是否请求成功</param>
/// <param name="statusCode">响应状态码</param>
public void Deconstruct(out TResult? result, out HttpResponseMessage httpResponseMessage,
out bool isSuccessStatusCode, out HttpStatusCode statusCode)
{
result = Result;
httpResponseMessage = ResponseMessage;
isSuccessStatusCode = IsSuccessStatusCode;
statusCode = StatusCode;
}
/// <summary> /// <summary>
/// 初始化 /// 初始化
/// </summary> /// </summary>
@@ -124,6 +198,16 @@ public sealed class HttpRemoteResult<TResult>
// 解析响应标头 Set-Cookie 集合 // 解析响应标头 Set-Cookie 集合
ParseSetCookies(ResponseMessage.Headers); ParseSetCookies(ResponseMessage.Headers);
// 获取 HTTP 版本
Version = ResponseMessage.Version;
// 获取 HttpClient 实例的配置名称
if (ResponseMessage.RequestMessage?.Options.TryGetValue(
new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME), out var httpClientName) == true)
{
HttpClientName = httpClientName;
}
} }
/// <summary> /// <summary>

View File

@@ -9,6 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Collections.ObjectModel;
using System.Text; using System.Text;
namespace ThingsGateway.HttpRemote; namespace ThingsGateway.HttpRemote;
@@ -19,6 +20,11 @@ namespace ThingsGateway.HttpRemote;
/// <remarks>参考文献https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events#%E5%AD%97%E6%AE%B5。</remarks> /// <remarks>参考文献https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events#%E5%AD%97%E6%AE%B5。</remarks>
public sealed class ServerSentEventsData public sealed class ServerSentEventsData
{ {
/// <summary>
/// 用于存储自定义的字段数据
/// </summary>
internal readonly List<KeyValuePair<string, string>> _customFields;
/// <summary> /// <summary>
/// 消息数据构建器 /// 消息数据构建器
/// </summary> /// </summary>
@@ -32,7 +38,11 @@ public sealed class ServerSentEventsData
/// <summary> /// <summary>
/// <inheritdoc cref="ServerSentEventsData" /> /// <inheritdoc cref="ServerSentEventsData" />
/// </summary> /// </summary>
internal ServerSentEventsData() => _dataBuffer = new StringBuilder(); internal ServerSentEventsData()
{
_dataBuffer = new StringBuilder();
_customFields = [];
}
/// <summary> /// <summary>
/// 事件类型 /// 事件类型
@@ -61,6 +71,12 @@ public sealed class ServerSentEventsData
/// <remarks>重新连接的时间。如果与服务器的连接丢失,浏览器将等待指定的时间,然后尝试重新连接。这必须是一个整数,以毫秒为单位指定重新连接的时间。如果指定了一个非整数值,该字段将被忽略。</remarks> /// <remarks>重新连接的时间。如果与服务器的连接丢失,浏览器将等待指定的时间,然后尝试重新连接。这必须是一个整数,以毫秒为单位指定重新连接的时间。如果指定了一个非整数值,该字段将被忽略。</remarks>
public int Retry { get; internal set; } public int Retry { get; internal set; }
/// <summary>
/// 自定义的字段数据
/// </summary>
public IReadOnlyCollection<KeyValuePair<string, string>> CustomFields =>
new ReadOnlyCollection<KeyValuePair<string, string>>(_customFields);
/// <summary> /// <summary>
/// 追加消息数据 /// 追加消息数据
/// </summary> /// </summary>
@@ -70,4 +86,12 @@ public sealed class ServerSentEventsData
_dataBuffer.Append(value); _dataBuffer.Append(value);
_cachedData = null; _cachedData = null;
} }
/// <summary>
/// 追加自定义字段数据
/// </summary>
/// <param name="name">字段名</param>
/// <param name="value">字段数据</param>
internal void AddCustomField(string name, string value) =>
_customFields.Add(new KeyValuePair<string, string>(name, value));
} }

View File

@@ -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;
/// <summary>
/// <see cref="HttpClient" /> 配置选项
/// </summary>
public sealed class HttpClientOptions
{
/// <summary>
/// JSON 序列化配置
/// </summary>
public JsonSerializerOptions JsonSerializerOptions { get; set; } =
new(HttpRemoteOptions.JsonSerializerOptionsDefault);
/// <summary>
/// 标识选项是否配置为默认值(未配置)
/// </summary>
/// <remarks>用于避免通过 <see cref="IOptionsSnapshot{TOptions}" /> 获取选项时无法确定是否已配置该选项。默认值为:<c>true</c>。</remarks>
internal bool IsDefault { get; set; } = true;
}

View File

@@ -12,9 +12,12 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ThingsGateway.Converters.Json;
namespace ThingsGateway.HttpRemote; namespace ThingsGateway.HttpRemote;
/// <summary> /// <summary>
@@ -30,7 +33,18 @@ public sealed class HttpRemoteOptions
{ {
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 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()
}
}; };
/// <summary> /// <summary>

View File

@@ -18,6 +18,7 @@ using System.Net.Http.Json;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using ThingsGateway.Extensions; 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) ? rawContent.ToCultureString(CultureInfo.InvariantCulture)
: JsonSerializer.Serialize(rawContent, : JsonSerializer.Serialize(rawContent,
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??

View File

@@ -51,7 +51,7 @@ internal sealed partial class HttpRemoteService
new FileDownloadManager(this, httpFileDownloadBuilder, configure).StartAsync(cancellationToken); new FileDownloadManager(this, httpFileDownloadBuilder, configure).StartAsync(cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage UploadFile(string? requestUri, string filePath, string name = "file", public HttpResponseMessage? UploadFile(string? requestUri, string filePath, string name = "file",
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null, Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null, Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
@@ -60,7 +60,7 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> UploadFileAsync(string? requestUri, string filePath, string name = "file", public Task<HttpResponseMessage?> UploadFileAsync(string? requestUri, string filePath, string name = "file",
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null, Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null, Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
@@ -69,12 +69,12 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Send(HttpFileUploadBuilder httpFileUploadBuilder, public HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
new FileUploadManager(this, httpFileUploadBuilder, configure).Start(cancellationToken); new FileUploadManager(this, httpFileUploadBuilder, configure).Start(cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> SendAsync(HttpFileUploadBuilder httpFileUploadBuilder, public Task<HttpResponseMessage?> SendAsync(HttpFileUploadBuilder httpFileUploadBuilder,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
new FileUploadManager(this, httpFileUploadBuilder, configure).StartAsync(cancellationToken); new FileUploadManager(this, httpFileUploadBuilder, configure).StartAsync(cancellationToken);

View File

@@ -17,22 +17,22 @@ namespace ThingsGateway.HttpRemote;
internal sealed partial class HttpRemoteService internal sealed partial class HttpRemoteService
{ {
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Get(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Get(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Get(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Get(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Get(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Get(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> GetAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> GetAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => GetAsync(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> GetAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> GetAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken);
@@ -58,22 +58,22 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Get<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Get<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Get<TResult>(requestUri, CancellationToken cancellationToken = default) => Get<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Get<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Get<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> GetAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> GetAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
GetAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); GetAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> GetAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, public Task<HttpRemoteResult<TResult>?> GetAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Get, requestUri, configure), completionOption,
cancellationToken); cancellationToken);
@@ -133,22 +133,22 @@ internal sealed partial class HttpRemoteService
GetAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); GetAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Put(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Put(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Put(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Put(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Put(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Put(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> PutAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> PutAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => PutAsync(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => PutAsync(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> PutAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> PutAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken);
@@ -174,22 +174,22 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Put<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Put<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Put<TResult>(requestUri, CancellationToken cancellationToken = default) => Put<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Put<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Put<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> PutAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> PutAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
PutAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); PutAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> PutAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, public Task<HttpRemoteResult<TResult>?> PutAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Put, requestUri, configure), completionOption,
cancellationToken); cancellationToken);
@@ -249,23 +249,23 @@ internal sealed partial class HttpRemoteService
PutAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); PutAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Post(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Post(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Post(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Post(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Post(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Post(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> PostAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> PostAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => PostAsync(requestUri, CancellationToken cancellationToken = default) => PostAsync(requestUri,
HttpCompletionOption.ResponseContentRead, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> PostAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> PostAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken);
@@ -291,22 +291,23 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Post<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Post<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Post<TResult>(requestUri, CancellationToken cancellationToken = default) => Post<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Post<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Post<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> PostAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> PostAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
PostAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); PostAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> PostAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, public Task<HttpRemoteResult<TResult>?> PostAsync<TResult>(string? requestUri,
HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Post, requestUri, configure), completionOption,
cancellationToken); cancellationToken);
@@ -366,23 +367,23 @@ internal sealed partial class HttpRemoteService
PostAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); PostAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Delete(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Delete(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Delete(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Delete(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Delete(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Delete(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> DeleteAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> DeleteAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => DeleteAsync(requestUri, CancellationToken cancellationToken = default) => DeleteAsync(requestUri,
HttpCompletionOption.ResponseContentRead, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> DeleteAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> DeleteAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken);
@@ -408,22 +409,22 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Delete<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Delete<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Delete<TResult>(requestUri, CancellationToken cancellationToken = default) => Delete<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Delete<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Delete<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> DeleteAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> DeleteAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
DeleteAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); DeleteAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> DeleteAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> DeleteAsync<TResult>(string? requestUri,
HttpCompletionOption completionOption, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Delete, requestUri, configure), completionOption,
@@ -487,23 +488,23 @@ internal sealed partial class HttpRemoteService
DeleteAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); DeleteAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Head(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Head(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Head(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Head(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Head(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Head(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> HeadAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> HeadAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => HeadAsync(requestUri, CancellationToken cancellationToken = default) => HeadAsync(requestUri,
HttpCompletionOption.ResponseContentRead, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> HeadAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> HeadAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken);
@@ -529,22 +530,23 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Head<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Head<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Head<TResult>(requestUri, CancellationToken cancellationToken = default) => Head<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Head<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Head<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> HeadAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> HeadAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
HeadAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HeadAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> HeadAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, public Task<HttpRemoteResult<TResult>?> HeadAsync<TResult>(string? requestUri,
HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Head, requestUri, configure), completionOption,
cancellationToken); cancellationToken);
@@ -604,23 +606,23 @@ internal sealed partial class HttpRemoteService
HeadAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); HeadAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Options(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Options(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Options(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Options(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Options(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Options(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> OptionsAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> OptionsAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => OptionsAsync(requestUri, CancellationToken cancellationToken = default) => OptionsAsync(requestUri,
HttpCompletionOption.ResponseContentRead, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> OptionsAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> OptionsAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken);
@@ -646,22 +648,22 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Options<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Options<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Options<TResult>(requestUri, CancellationToken cancellationToken = default) => Options<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Options<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Options<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> OptionsAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> OptionsAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
OptionsAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); OptionsAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> OptionsAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> OptionsAsync<TResult>(string? requestUri,
HttpCompletionOption completionOption, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Options, requestUri, configure), completionOption,
@@ -725,23 +727,23 @@ internal sealed partial class HttpRemoteService
OptionsAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); OptionsAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Trace(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Trace(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Trace(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Trace(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Trace(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Trace(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> TraceAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> TraceAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => TraceAsync(requestUri, CancellationToken cancellationToken = default) => TraceAsync(requestUri,
HttpCompletionOption.ResponseContentRead, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> TraceAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> TraceAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken);
@@ -767,22 +769,22 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Trace<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Trace<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Trace<TResult>(requestUri, CancellationToken cancellationToken = default) => Trace<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Trace<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Trace<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> TraceAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> TraceAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
TraceAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); TraceAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> TraceAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> TraceAsync<TResult>(string? requestUri,
HttpCompletionOption completionOption, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Trace, requestUri, configure), completionOption,
@@ -846,23 +848,23 @@ internal sealed partial class HttpRemoteService
TraceAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken); TraceAsAsync<byte[]>(requestUri, completionOption, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Patch(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpResponseMessage? Patch(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Patch(requestUri, HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) => Patch(requestUri, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Patch(string? requestUri, HttpCompletionOption completionOption, public HttpResponseMessage? Patch(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send(
HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> PatchAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, public Task<HttpResponseMessage?> PatchAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => PatchAsync(requestUri, CancellationToken cancellationToken = default) => PatchAsync(requestUri,
HttpCompletionOption.ResponseContentRead, HttpCompletionOption.ResponseContentRead,
configure, cancellationToken); configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> PatchAsync(string? requestUri, HttpCompletionOption completionOption, public Task<HttpResponseMessage?> PatchAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => SendAsync(
HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken);
@@ -888,22 +890,22 @@ internal sealed partial class HttpRemoteService
cancellationToken); cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Patch<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, public HttpRemoteResult<TResult>? Patch<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default) => Patch<TResult>(requestUri, CancellationToken cancellationToken = default) => Patch<TResult>(requestUri,
HttpCompletionOption.ResponseContentRead, configure, cancellationToken); HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Patch<TResult>(string? requestUri, HttpCompletionOption completionOption, public HttpRemoteResult<TResult>? Patch<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>( Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Send<TResult>(
HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken); HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> PatchAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> PatchAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
PatchAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken); PatchAsync<TResult>(requestUri, HttpCompletionOption.ResponseContentRead, configure, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> PatchAsync<TResult>(string? requestUri, public Task<HttpRemoteResult<TResult>?> PatchAsync<TResult>(string? requestUri,
HttpCompletionOption completionOption, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) => Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption, SendAsync<TResult>(HttpRequestBuilder.Create(HttpMethod.Patch, requestUri, configure), completionOption,

View File

@@ -90,16 +90,16 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
public IServiceProvider ServiceProvider { get; } public IServiceProvider ServiceProvider { get; }
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, public HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, public HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, _) = SendCoreAsync(httpRequestBuilder, completionOption, default, var (httpResponseMessage, _) = SendCoreAsync(httpRequestBuilder, completionOption, null,
(httpClient, httpRequestMessage, option, token) => (httpClient, httpRequestMessage, option, token) =>
httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult();
@@ -107,18 +107,18 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseMessage> SendAsync(HttpRequestBuilder httpRequestBuilder, public Task<HttpResponseMessage?> SendAsync(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public async Task<HttpResponseMessage> SendAsync(HttpRequestBuilder httpRequestBuilder, public async Task<HttpResponseMessage?> SendAsync(HttpRequestBuilder httpRequestBuilder,
HttpCompletionOption completionOption, CancellationToken cancellationToken = default) HttpCompletionOption completionOption, CancellationToken cancellationToken = default)
{ {
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, _) = await SendCoreAsync(httpRequestBuilder, completionOption, var (httpResponseMessage, _) = await SendCoreAsync(httpRequestBuilder, completionOption,
(httpClient, httpRequestMessage, option, token) => (httpClient, httpRequestMessage, option, token) =>
httpClient.SendAsync(httpRequestMessage, option, token), default, cancellationToken).ConfigureAwait(false); httpClient.SendAsync(httpRequestMessage, option, token), null, cancellationToken).ConfigureAwait(false);
return httpResponseMessage; return httpResponseMessage;
} }
@@ -133,7 +133,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, default, var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, null,
(httpClient, httpRequestMessage, option, token) => (httpClient, httpRequestMessage, option, token) =>
httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult();
@@ -156,7 +156,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
cancellationToken); cancellationToken);
// 动态创建 HttpRemoteResult<TResult> 实例并转换为 TResult 实例 // 动态创建 HttpRemoteResult<TResult> 实例并转换为 TResult 实例
return (TResult)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration); return (TResult?)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -199,7 +199,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption, var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption,
(httpClient, httpRequestMessage, option, token) => (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); var resultType = typeof(TResult);
@@ -220,7 +220,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
// 动态创建 HttpRemoteResult<TResult> 实例并转换为 TResult 实例 // 动态创建 HttpRemoteResult<TResult> 实例并转换为 TResult 实例
return (TResult)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration); return (TResult?)DynamicCreateHttpRemoteResult(resultType, httpResponseMessage, result, requestDuration);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -263,7 +263,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, default, var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, null,
(httpClient, httpRequestMessage, option, token) => (httpClient, httpRequestMessage, option, token) =>
httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult();
@@ -298,7 +298,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption, var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption,
(httpClient, httpRequestMessage, option, token) => (httpClient, httpRequestMessage, option, token) =>
httpClient.SendAsync(httpRequestMessage, option, token), default, cancellationToken).ConfigureAwait(false); httpClient.SendAsync(httpRequestMessage, option, token), null, cancellationToken).ConfigureAwait(false);
// 检查类型是否是 HttpRemoteResult<TResult> 类型 // 检查类型是否是 HttpRemoteResult<TResult> 类型
if (!typeof(HttpRemoteResult<>).IsDefinitionEqual(resultType)) if (!typeof(HttpRemoteResult<>).IsDefinitionEqual(resultType))
@@ -320,19 +320,25 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
} }
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Send<TResult>(HttpRequestBuilder httpRequestBuilder, public HttpRemoteResult<TResult>? Send<TResult>(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
Send<TResult>(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); Send<TResult>(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public HttpRemoteResult<TResult> Send<TResult>(HttpRequestBuilder httpRequestBuilder, public HttpRemoteResult<TResult>? Send<TResult>(HttpRequestBuilder httpRequestBuilder,
HttpCompletionOption completionOption, CancellationToken cancellationToken = default) HttpCompletionOption completionOption, CancellationToken cancellationToken = default)
{ {
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, default, var (httpResponseMessage, requestDuration) = SendCoreAsync(httpRequestBuilder, completionOption, null,
(httpClient, httpRequestMessage, option, token) => (httpClient, httpRequestMessage, option, token) =>
httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult(); httpClient.Send(httpRequestMessage, option, token), cancellationToken).GetAwaiter().GetResult();
// 空检查
if (httpResponseMessage is null)
{
return null;
}
// 将 HttpResponseMessage 转换为 TResult 实例 // 将 HttpResponseMessage 转换为 TResult 实例
var result = _httpContentConverterFactory.Read<TResult>(httpResponseMessage, var result = _httpContentConverterFactory.Read<TResult>(httpResponseMessage,
httpRequestBuilder.HttpContentConverterProviders?.SelectMany(u => u.Invoke()).ToArray(), cancellationToken); httpRequestBuilder.HttpContentConverterProviders?.SelectMany(u => u.Invoke()).ToArray(), cancellationToken);
@@ -348,18 +354,24 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpRemoteResult<TResult>> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder, public Task<HttpRemoteResult<TResult>?> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default) => SendAsync<TResult>(httpRequestBuilder, CancellationToken cancellationToken = default) => SendAsync<TResult>(httpRequestBuilder,
HttpCompletionOption.ResponseContentRead, cancellationToken); HttpCompletionOption.ResponseContentRead, cancellationToken);
/// <inheritdoc /> /// <inheritdoc />
public async Task<HttpRemoteResult<TResult>> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder, public async Task<HttpRemoteResult<TResult>?> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder,
HttpCompletionOption completionOption, CancellationToken cancellationToken = default) HttpCompletionOption completionOption, CancellationToken cancellationToken = default)
{ {
// 发送 HTTP 远程请求 // 发送 HTTP 远程请求
var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption, var (httpResponseMessage, requestDuration) = await SendCoreAsync(httpRequestBuilder, completionOption,
(httpClient, httpRequestMessage, option, token) => (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 实例 // 将 HttpResponseMessage 转换为 TResult 实例
var result = await _httpContentConverterFactory.ReadAsync<TResult>(httpResponseMessage, var result = await _httpContentConverterFactory.ReadAsync<TResult>(httpResponseMessage,
@@ -392,7 +404,8 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="Tuple{T1, T2}" /> /// <see cref="Tuple{T1, T2}" />
/// </returns> /// </returns>
internal async Task<(HttpResponseMessage ResponseMessage, long RequestDuration)> SendCoreAsync( /// <exception cref="InvalidOperationException"></exception>
internal async Task<(HttpResponseMessage? ResponseMessage, long RequestDuration)> SendCoreAsync(
HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption,
Func<HttpClient, HttpRequestMessage, HttpCompletionOption, CancellationToken, Task<HttpResponseMessage>>? Func<HttpClient, HttpRequestMessage, HttpCompletionOption, CancellationToken, Task<HttpResponseMessage>>?
sendAsyncMethod, sendAsyncMethod,
@@ -445,6 +458,20 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
// 设置单次请求超时时间控制 // 设置单次请求超时时间控制
if (httpRequestBuilder.Timeout is not null && httpRequestBuilder.Timeout.Value != TimeSpan.Zero) 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); timeoutCancellationTokenSource.CancelAfter(httpRequestBuilder.Timeout.Value);
} }
@@ -534,7 +561,13 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
// 处理发送 HTTP 请求发生异常 // 处理发送 HTTP 请求发生异常
HandleRequestFailed(httpRequestBuilder, requestEventHandler, e, httpResponseMessage); HandleRequestFailed(httpRequestBuilder, requestEventHandler, e, httpResponseMessage);
throw; // 检查是否启用异常抑制机制
if (!ShouldSuppressException(httpRequestBuilder.SuppressExceptionTypes, e))
{
throw;
}
return (httpResponseMessage, requestDuration);
} }
finally finally
{ {
@@ -706,7 +739,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(httpClient);
// 添加默认的 User-Agent 标头 // 添加默认的 User-Agent 标头
AddDefaultUserAgentHeader(httpClient); AddDefaultUserAgentHeader(httpClient, httpRequestBuilder);
// 存储 HttpClientPooling 实例并返回 // 存储 HttpClientPooling 实例并返回
return httpRequestBuilder.HttpClientPooling = new HttpClientPooling(httpClient, release); return httpRequestBuilder.HttpClientPooling = new HttpClientPooling(httpClient, release);
@@ -719,10 +752,15 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
/// <param name="httpClient"> /// <param name="httpClient">
/// <see cref="HttpClient" /> /// <see cref="HttpClient" />
/// </param> /// </param>
internal static void AddDefaultUserAgentHeader(HttpClient httpClient) /// <param name="httpRequestBuilder">
/// <see cref="HttpRequestBuilder" />
/// </param>
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; return;
} }
@@ -854,10 +892,10 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
int.TryParse(stringStatusCode, out var intStatusCodeResult) && int.TryParse(stringStatusCode, out var intStatusCodeResult) &&
intStatusCodeResult == statusCode: intStatusCodeResult == statusCode:
return true; return true;
// 处理字符串区间类型,如 200-500 // 处理字符串区间类型,如 200-500 或 200~500
case string stringStatusCode when StatusCodeRangeRegex().IsMatch(stringStatusCode): 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)) 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, "=" => statusCode == number,
_ => false _ => false
}; };
default:
return false;
} }
return false; return false;
@@ -908,9 +944,8 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
/// <see cref="object" /> /// <see cref="object" />
/// </returns> /// </returns>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
internal static object DynamicCreateHttpRemoteResult(Type httpRemoteResultType, internal static object? DynamicCreateHttpRemoteResult(Type httpRemoteResultType,
HttpResponseMessage httpResponseMessage, HttpResponseMessage? httpResponseMessage, object? result, long requestDuration)
object? result, long requestDuration)
{ {
// 检查类型是否是 HttpRemoteResult<TResult> 类型 // 检查类型是否是 HttpRemoteResult<TResult> 类型
if (!typeof(HttpRemoteResult<>).IsDefinitionEqual(httpRemoteResultType)) if (!typeof(HttpRemoteResult<>).IsDefinitionEqual(httpRemoteResultType))
@@ -920,6 +955,12 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
nameof(httpRemoteResultType)); nameof(httpRemoteResultType));
} }
// 空检查
if (httpResponseMessage is null)
{
return null;
}
// 反射创建 HttpRemoteResult<TResult> 实例 // 反射创建 HttpRemoteResult<TResult> 实例
var httpRemoteResult = Activator.CreateInstance(httpRemoteResultType, httpResponseMessage); var httpRemoteResult = Activator.CreateInstance(httpRemoteResultType, httpResponseMessage);
@@ -946,11 +987,32 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
return httpRemoteResult; return httpRemoteResult;
} }
/// <summary>
/// 检查是否启用异常抑制机制
/// </summary>
/// <param name="suppressExceptionTypes">受抑制的异常类型列表</param>
/// <param name="exception">
/// <see cref="Exception" />
/// </param>
/// <returns>
/// <see cref="bool" />
/// </returns>
internal static bool ShouldSuppressException(HashSet<Type>? suppressExceptionTypes, Exception? exception)
{
// 空检查
if (suppressExceptionTypes is null or { Count: 0 } || exception is null)
{
return false;
}
return suppressExceptionTypes.Any(u => u.IsInstanceOfType(exception));
}
/// <summary> /// <summary>
/// 状态码区间正则表达式 /// 状态码区间正则表达式
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[GeneratedRegex(@"^\d+-\d+$")] [GeneratedRegex(@"^\d+[-~]\d+$")]
private static partial Regex StatusCodeRangeRegex(); private static partial Regex StatusCodeRangeRegex();
/// <summary> /// <summary>

View File

@@ -106,7 +106,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage UploadFile(string? requestUri, string filePath, string name = "file", HttpResponseMessage? UploadFile(string? requestUri, string filePath, string name = "file",
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null, Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null, Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -127,7 +127,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="Task{TResult}" /> /// <see cref="Task{TResult}" />
/// </returns> /// </returns>
Task<HttpResponseMessage> UploadFileAsync(string? requestUri, string filePath, string name = "file", Task<HttpResponseMessage?> UploadFileAsync(string? requestUri, string filePath, string name = "file",
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null, Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null, Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -145,7 +145,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Send(HttpFileUploadBuilder httpFileUploadBuilder, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -161,7 +161,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="Task{TResult}" /> /// <see cref="Task{TResult}" />
/// </returns> /// </returns>
Task<HttpResponseMessage> SendAsync(HttpFileUploadBuilder httpFileUploadBuilder, Task<HttpResponseMessage?> SendAsync(HttpFileUploadBuilder httpFileUploadBuilder,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>

View File

@@ -27,7 +27,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Get(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Get(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -44,7 +44,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Get(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Get(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -58,7 +58,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> GetAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> GetAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -75,7 +75,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> GetAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> GetAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -156,7 +156,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Get<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Get<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -174,7 +174,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Get<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Get<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -189,7 +189,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> GetAsync<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpRemoteResult<TResult>?> GetAsync<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -207,7 +207,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> GetAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> GetAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -407,7 +407,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Put(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Put(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -424,7 +424,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Put(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Put(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -438,7 +438,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> PutAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> PutAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -455,7 +455,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> PutAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> PutAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -536,7 +536,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Put<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Put<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -554,7 +554,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Put<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Put<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -569,7 +569,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> PutAsync<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpRemoteResult<TResult>?> PutAsync<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -587,7 +587,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> PutAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> PutAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -787,7 +787,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Post(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Post(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -804,7 +804,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Post(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Post(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -818,7 +818,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> PostAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> PostAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -835,7 +835,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> PostAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> PostAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -916,7 +916,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Post<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Post<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -934,7 +934,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Post<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Post<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -949,7 +949,8 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> PostAsync<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpRemoteResult<TResult>?> PostAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -967,7 +968,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> PostAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> PostAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1167,7 +1168,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Delete(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Delete(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1184,7 +1185,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Delete(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Delete(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1198,7 +1199,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> DeleteAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> DeleteAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1215,7 +1216,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> DeleteAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> DeleteAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1296,7 +1297,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Delete<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Delete<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1314,7 +1315,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Delete<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Delete<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1329,7 +1330,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> DeleteAsync<TResult>(string? requestUri, Task<HttpRemoteResult<TResult>?> DeleteAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -1348,7 +1349,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> DeleteAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> DeleteAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1548,7 +1549,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Head(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Head(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1565,7 +1566,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Head(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Head(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1579,7 +1580,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> HeadAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> HeadAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1596,7 +1597,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> HeadAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> HeadAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1677,7 +1678,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Head<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Head<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1695,7 +1696,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Head<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Head<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1710,7 +1711,8 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> HeadAsync<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpRemoteResult<TResult>?> HeadAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1728,7 +1730,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> HeadAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> HeadAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1928,7 +1930,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Options(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Options(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1945,7 +1947,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Options(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Options(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1959,7 +1961,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> OptionsAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> OptionsAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -1976,7 +1978,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> OptionsAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> OptionsAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2057,7 +2059,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Options<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Options<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2075,7 +2077,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Options<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Options<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2090,7 +2092,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> OptionsAsync<TResult>(string? requestUri, Task<HttpRemoteResult<TResult>?> OptionsAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -2109,7 +2111,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> OptionsAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> OptionsAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2309,7 +2311,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Trace(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Trace(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2326,7 +2328,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Trace(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Trace(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2340,7 +2342,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> TraceAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> TraceAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2357,7 +2359,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> TraceAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> TraceAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2438,7 +2440,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Trace<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Trace<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2456,7 +2458,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Trace<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Trace<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2471,7 +2473,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> TraceAsync<TResult>(string? requestUri, Task<HttpRemoteResult<TResult>?> TraceAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -2490,7 +2492,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> TraceAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> TraceAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2690,7 +2692,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Patch(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpResponseMessage? Patch(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2707,7 +2709,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Patch(string? requestUri, HttpCompletionOption completionOption, HttpResponseMessage? Patch(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2721,7 +2723,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> PatchAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null, Task<HttpResponseMessage?> PatchAsync(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2738,7 +2740,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> PatchAsync(string? requestUri, HttpCompletionOption completionOption, Task<HttpResponseMessage?> PatchAsync(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2819,7 +2821,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Patch<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null, HttpRemoteResult<TResult>? Patch<TResult>(string? requestUri, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2837,7 +2839,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Patch<TResult>(string? requestUri, HttpCompletionOption completionOption, HttpRemoteResult<TResult>? Patch<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -2852,7 +2854,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> PatchAsync<TResult>(string? requestUri, Task<HttpRemoteResult<TResult>?> PatchAsync<TResult>(string? requestUri,
Action<HttpRequestBuilder>? configure = null, Action<HttpRequestBuilder>? configure = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -2871,7 +2873,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> PatchAsync<TResult>(string? requestUri, HttpCompletionOption completionOption, Task<HttpRemoteResult<TResult>?> PatchAsync<TResult>(string? requestUri, HttpCompletionOption completionOption,
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default); Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>

View File

@@ -33,7 +33,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default); HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 发送 HTTP 远程请求 /// 发送 HTTP 远程请求
@@ -50,7 +50,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
HttpResponseMessage Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, HttpResponseMessage? Send(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -65,7 +65,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> SendAsync(HttpRequestBuilder httpRequestBuilder, Task<HttpResponseMessage?> SendAsync(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -83,7 +83,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpResponseMessage" /> /// <see cref="HttpResponseMessage" />
/// </returns> /// </returns>
Task<HttpResponseMessage> SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption, Task<HttpResponseMessage?> SendAsync(HttpRequestBuilder httpRequestBuilder, HttpCompletionOption completionOption,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -433,7 +433,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Send<TResult>(HttpRequestBuilder httpRequestBuilder, HttpRemoteResult<TResult>? Send<TResult>(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -452,7 +452,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
HttpRemoteResult<TResult> Send<TResult>(HttpRequestBuilder httpRequestBuilder, HttpRemoteResult<TResult>? Send<TResult>(HttpRequestBuilder httpRequestBuilder,
HttpCompletionOption completionOption, CancellationToken cancellationToken = default); HttpCompletionOption completionOption, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -468,7 +468,7 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder, Task<HttpRemoteResult<TResult>?> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -487,6 +487,6 @@ public partial interface IHttpRemoteService
/// <returns> /// <returns>
/// <see cref="HttpRemoteResult{TResult}" /> /// <see cref="HttpRemoteResult{TResult}" />
/// </returns> /// </returns>
Task<HttpRemoteResult<TResult>> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder, Task<HttpRemoteResult<TResult>?> SendAsync<TResult>(HttpRequestBuilder httpRequestBuilder,
HttpCompletionOption completionOption, CancellationToken cancellationToken = default); HttpCompletionOption completionOption, CancellationToken cancellationToken = default);
} }

View File

@@ -41,7 +41,7 @@ public static class HttpRemoteUtility
/// 忽略 SSL 证书验证 /// 忽略 SSL 证书验证
/// </summary> /// </summary>
public static Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> IgnoreSslErrors => public static Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> IgnoreSslErrors =>
(message, cert, chain, errors) => true; (_, _, _, _) => true;
/// <summary> /// <summary>
/// 获取使用 IPv4 连接到服务器的回调 /// 获取使用 IPv4 连接到服务器的回调
@@ -118,8 +118,20 @@ public static class HttpRemoteUtility
// - IPv4: AddressFamily.InterNetwork // - IPv4: AddressFamily.InterNetwork
// - IPv6: AddressFamily.InterNetworkV6 // - IPv6: AddressFamily.InterNetworkV6
// - IPv4 或 IPv6: AddressFamily.Unspecified // - 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); var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
@@ -129,7 +141,7 @@ public static class HttpRemoteUtility
try try
{ {
await socket.ConnectAsync(entry.AddressList, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false); await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false);
// 如果你想选择特定的 IP 地址来连接服务器 // 如果你想选择特定的 IP 地址来连接服务器
// await socket.ConnectAsync( // await socket.ConnectAsync(

View File

@@ -34,5 +34,5 @@ public sealed class WebSocketBinaryReceiveResult : WebSocketReceiveResult
/// <summary> /// <summary>
/// 二进制消息 /// 二进制消息
/// </summary> /// </summary>
public byte[] Message { get; internal init; } = default!; public byte[] Message { get; internal init; } = null!;
} }

View File

@@ -34,5 +34,5 @@ public sealed class WebSocketTextReceiveResult : WebSocketReceiveResult
/// <summary> /// <summary>
/// 文本消息 /// 文本消息
/// </summary> /// </summary>
public string Message { get; internal init; } = default!; public string Message { get; internal init; } = null!;
} }

View File

@@ -329,6 +329,7 @@ public class FallbackPolicy<TResult> : PolicyBase<TResult>
{ {
// 获取操作方法执行结果 // 获取操作方法执行结果
context.Result = await operation(cancellationToken).ConfigureAwait(false); context.Result = await operation(cancellationToken).ConfigureAwait(false);
context.Exception = null;
} }
catch (System.Exception exception) catch (System.Exception exception)
{ {

View File

@@ -391,6 +391,7 @@ public class RetryPolicy<TResult> : PolicyBase<TResult>
{ {
// 获取操作方法执行结果 // 获取操作方法执行结果
context.Result = await operation(cancellationToken).ConfigureAwait(false); context.Result = await operation(cancellationToken).ConfigureAwait(false);
context.Exception = null;
} }
catch (System.Exception exception) catch (System.Exception exception)
{ {

View File

@@ -0,0 +1,19 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.Shapeless;
/// <summary>
/// 流变对象模型绑定特性
/// </summary>
/// <remarks>示例代码:<c>[Clay] dynamic input</c>。</remarks>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ClayAttribute : Attribute;

View File

@@ -14,7 +14,12 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection; using System.Reflection;
using System.Web;
using ThingsGateway.Extensions;
namespace ThingsGateway.Shapeless; namespace ThingsGateway.Shapeless;
@@ -35,9 +40,13 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
// 获取 HttpContext 实例 // 获取 HttpContext 实例
var httpContext = bindingContext.HttpContext; var httpContext = bindingContext.HttpContext;
// 检查是否是 URL 表单application/x-www-form-urlencoded内容
var isFormUrlEncoded = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType!).MediaType ==
MediaTypeNames.Application.FormUrlEncoded;
// 尝试从请求体中读取数据,并将其转换为 Clay 实例 // 尝试从请求体中读取数据,并将其转换为 Clay 实例
var (canParse, model) = var (canParse, model) = await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options.Value,
await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options.Value, httpContext.RequestAborted).ConfigureAwait(false); isFormUrlEncoded, httpContext.RequestAborted).ConfigureAwait(false);
bindingContext.Result = !canParse ? ModelBindingResult.Failed() : ModelBindingResult.Success(model); bindingContext.Result = !canParse ? ModelBindingResult.Failed() : ModelBindingResult.Success(model);
} }
@@ -49,6 +58,7 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
/// <param name="options"> /// <param name="options">
/// <see cref="ClayOptions" /> /// <see cref="ClayOptions" />
/// </param> /// </param>
/// <param name="isFormUrlEncoded">是否是 <c>application/x-www-form-urlencoded</c> 表单</param>
/// <param name="cancellationToken"> /// <param name="cancellationToken">
/// <see cref="CancellationToken" /> /// <see cref="CancellationToken" />
/// </param> /// </param>
@@ -56,7 +66,7 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
/// <see cref="Tuple{T1,T2}" /> /// <see cref="Tuple{T1,T2}" />
/// </returns> /// </returns>
internal static async Task<(bool canParse, Clay? model)> TryReadAndConvertBodyToClayAsync(Stream stream, internal static async Task<(bool canParse, Clay? model)> TryReadAndConvertBodyToClayAsync(Stream stream,
ClayOptions options, CancellationToken cancellationToken) ClayOptions options, bool isFormUrlEncoded, CancellationToken cancellationToken)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(stream);
@@ -65,7 +75,11 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
using var streamReader = new StreamReader(stream); using var streamReader = new StreamReader(stream);
var json = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); 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));
} }
/// <summary> /// <summary>
@@ -89,9 +103,13 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
// 解析 ClayOptions 选项 // 解析 ClayOptions 选项
var options = httpContext.RequestServices.GetRequiredService<IOptions<ClayOptions>>().Value; var options = httpContext.RequestServices.GetRequiredService<IOptions<ClayOptions>>().Value;
// 检查是否是 URL 表单application/x-www-form-urlencoded内容
var isFormUrlEncoded = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType!).MediaType ==
MediaTypeNames.Application.FormUrlEncoded;
// 尝试从请求体流中读取数据,并将其转换为 Clay 实例 // 尝试从请求体流中读取数据,并将其转换为 Clay 实例
var (_, model) = var (_, model) = await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options, isFormUrlEncoded,
await TryReadAndConvertBodyToClayAsync(httpContext.Request.Body, options, httpContext.RequestAborted).ConfigureAwait(false); httpContext.RequestAborted).ConfigureAwait(false);
return model; return model;
} }

View File

@@ -11,6 +11,9 @@
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using System.Runtime.CompilerServices;
namespace ThingsGateway.Shapeless; namespace ThingsGateway.Shapeless;
@@ -25,6 +28,15 @@ internal sealed class ClayBinderProvider : IModelBinderProvider
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(context); 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<ClayAttribute>().Any() == true &&
parameterAttributes.OfType<DynamicAttribute>().Any())
? new BinderTypeModelBinder(typeof(ClayBinder))
: null;
} }
} }

View File

@@ -37,6 +37,11 @@ public partial class Clay
/// </summary> /// </summary>
public IEnumerable<object> Keys => AsEnumerable().Select(u => u.Key); public IEnumerable<object> Keys => AsEnumerable().Select(u => u.Key);
/// <summary>
/// 获取单一对象键(属性名)的列表
/// </summary>
public IEnumerable<string> MemberNames => AsEnumerateObject().Select(u => u.Key);
/// <summary> /// <summary>
/// 获取值或元素的列表 /// 获取值或元素的列表
/// </summary> /// </summary>
@@ -127,6 +132,17 @@ public partial class Clay
} }
} }
/// <summary>
/// 将流变对象转换为 <see cref="Dictionary{TKey,TValue}" />
/// </summary>
/// <returns>
/// <see cref="Dictionary{TKey,TValue}" />
/// </returns>
public Dictionary<string, dynamic?> ToDictionary() =>
IsObject
? As<Dictionary<string, dynamic?>>()!
: As<Dictionary<int, dynamic?>>()!.ToDictionary(u => u.Key.ToString(), u => u.Value);
/// <summary> /// <summary>
/// 遍历 <see cref="Clay" /> /// 遍历 <see cref="Clay" />
/// </summary> /// </summary>

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
@@ -86,6 +87,14 @@ public partial class Clay
/// </param> /// </param>
public Clay this[Range range] => (Clay)this[range as object]!; public Clay this[Range range] => (Clay)this[range as object]!;
/// <summary>
/// 路径索引
/// </summary>
/// <remarks>根据路径获取值。</remarks>
/// <param name="identifier">带路径的标识符</param>
/// <param name="isPath">是否是带路径的标识符</param>
public object? this[string identifier, bool isPath] => isPath ? PathValue(identifier) : GetValue(identifier);
/// <summary> /// <summary>
/// 判断是否为单一对象 /// 判断是否为单一对象
/// </summary> /// </summary>
@@ -155,6 +164,35 @@ public partial class Clay
return ToJsonString(jsonSerializerOptions); return ToJsonString(jsonSerializerOptions);
} }
/// <summary>
/// 解构函数
/// </summary>
/// <param name="clay">dynamic 类型的 <see cref="Clay" /></param>
/// <param name="enumerableClay">
/// <see cref="IEnumerable{T}" />
/// </param>
public void Deconstruct(out dynamic clay, out IEnumerable<dynamic?> enumerableClay)
{
clay = this;
enumerableClay = this;
}
/// <summary>
/// </summary>
/// <param name="clay">dynamic 类型的 <see cref="Clay" /></param>
/// <param name="enumerableClay">
/// <see cref="IEnumerable{T}" />
/// </param>
/// <param name="rawClay">
/// <see cref="Clay" />
/// </param>
public void Deconstruct(out dynamic clay, out IEnumerable<dynamic?> enumerableClay, out Clay rawClay)
{
clay = this;
enumerableClay = this;
rawClay = this;
}
/// <summary> /// <summary>
/// 创建空的单一对象 /// 创建空的单一对象
/// </summary> /// </summary>
@@ -260,6 +298,38 @@ public partial class Clay
public static Clay Parse(ref Utf8JsonReader utf8JsonReader, Action<ClayOptions> configure) => public static Clay Parse(ref Utf8JsonReader utf8JsonReader, Action<ClayOptions> configure) =>
Parse(ref utf8JsonReader, ClayOptions.Default.Configure(configure)); Parse(ref utf8JsonReader, ClayOptions.Default.Configure(configure));
/// <summary>
/// 从文件中读取数据并转换为 <see cref="Clay" /> 实例
/// </summary>
/// <param name="path">文件路径</param>
/// <param name="options">
/// <see cref="ClayOptions" />
/// </param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
public static Clay ParseFromFile(string path, ClayOptions? options = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(path);
// 打开文件并读取流
using var fileStream = File.OpenRead(path);
return Parse(fileStream, options);
}
/// <summary>
/// 从文件中读取数据并转换为 <see cref="Clay" /> 实例
/// </summary>
/// <param name="path">文件路径</param>
/// <param name="configure">自定义配置委托</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
public static Clay ParseFromFile(string path, Action<ClayOptions> configure) =>
ParseFromFile(path, ClayOptions.Default.Configure(configure));
/// <summary> /// <summary>
/// 检查标识符是否定义 /// 检查标识符是否定义
/// </summary> /// </summary>
@@ -318,6 +388,37 @@ public partial class Clay
/// </returns> /// </returns>
public bool IsDefined(object identifier) => Contains(identifier); public bool IsDefined(object identifier) => Contains(identifier);
/// <summary>
/// 检查属性(键)是否定义
/// </summary>
/// <param name="propertyName">属性名(键)</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
public bool HasProperty(string propertyName)
{
// 检查是否是集合或数组实例调用
ThrowIfMethodCalledOnArrayCollection(nameof(HasProperty));
return Contains(propertyName);
}
/// <summary>
/// 获取集合或数组中指定项(元素)的索引
/// </summary>
/// <param name="value">项(元素)</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
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;
}
/// <summary> /// <summary>
/// 根据标识符获取值 /// 根据标识符获取值
/// </summary> /// </summary>
@@ -375,6 +476,12 @@ public partial class Clay
// 根据标识符查找 JsonNode 节点 // 根据标识符查找 JsonNode 节点
var jsonNode = FindNode(identifier); var jsonNode = FindNode(identifier);
// 处理 object 类型生成 JsonElement 问题
if (resultType == typeof(object))
{
return DeserializeNode(jsonNode, Options);
}
return IsClay(resultType) return IsClay(resultType)
? new Clay(jsonNode, Options) ? new Clay(jsonNode, Options)
: Helpers.DeserializeNode(jsonNode, resultType, jsonSerializerOptions ?? Options.JsonSerializerOptions); : Helpers.DeserializeNode(jsonNode, resultType, jsonSerializerOptions ?? Options.JsonSerializerOptions);
@@ -394,6 +501,94 @@ public partial class Clay
public TResult? Get<TResult>(object identifier, JsonSerializerOptions? jsonSerializerOptions = null) => public TResult? Get<TResult>(object identifier, JsonSerializerOptions? jsonSerializerOptions = null) =>
(TResult?)Get(identifier, typeof(TResult), jsonSerializerOptions); (TResult?)Get(identifier, typeof(TResult), jsonSerializerOptions);
/// <summary>
/// 根据路径获取值
/// </summary>
/// <remarks>不支持获取自定义委托。</remarks>
/// <param name="path">带路径的标识符</param>
/// <returns>
/// <see cref="object" />
/// </returns>
public object? PathValue(string path) => PathValue<object>(path);
/// <summary>
/// 根据路径获取值
/// </summary>
/// <remarks>不支持获取自定义委托。</remarks>
/// <param name="path">带路径的标识符</param>
/// <param name="resultType">转换的目标类型</param>
/// <param name="jsonSerializerOptions">
/// <see cref="JsonSerializerOptions" />
/// </param>
/// <returns>
/// <see cref="object" />
/// </returns>
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);
}
/// <summary>
/// 根据路径获取值
/// </summary>
/// <remarks>不支持获取自定义委托。</remarks>
/// <param name="path">带路径的标识符</param>
/// <param name="jsonSerializerOptions">
/// <see cref="JsonSerializerOptions" />
/// </param>
/// <typeparam name="TResult">转换的目标类型</typeparam>
/// <returns>
/// <typeparamref name="TResult" />
/// </returns>
public TResult? PathValue<TResult>(string path, JsonSerializerOptions? jsonSerializerOptions = null) =>
(TResult?)PathValue(path, typeof(TResult), jsonSerializerOptions);
/// <summary> /// <summary>
/// 根据标识符查找 <see cref="JsonNode" /> 节点 /// 根据标识符查找 <see cref="JsonNode" /> 节点
/// </summary> /// </summary>
@@ -641,7 +836,7 @@ public partial class Clay
throw new ArgumentException("Clay array contains one or more null elements.", nameof(clays)); throw new ArgumentException("Clay array contains one or more null elements.", nameof(clays));
} }
// 检查流变对象类型是否一致 // 检查流变对象类型是否一致
if (clays.Any(u => u.Type != Type)) if (clays.Any(u => u.Type != Type))
{ {
throw new InvalidOperationException("All Clay objects must be of the same type."); throw new InvalidOperationException("All Clay objects must be of the same type.");
@@ -668,6 +863,51 @@ public partial class Clay
return combineClay; return combineClay;
} }
/// <summary>
/// 拓展属性或项
/// </summary>
/// <param name="values">值集合</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
/// <exception cref="InvalidOperationException"></exception>
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;
}
/// <summary> /// <summary>
/// 根据标识符删除数据 /// 根据标识符删除数据
/// </summary> /// </summary>
@@ -733,8 +973,8 @@ public partial class Clay
/// </returns> /// </returns>
public object? As(Type resultType, JsonSerializerOptions? jsonSerializerOptions = null) public object? As(Type resultType, JsonSerializerOptions? jsonSerializerOptions = null)
{ {
// 检查是否是 Clay 类型或 IEnumerable<dynamic?> 类型 // 检查是否是 Clay 类型或 IEnumerable<dynamic?> 类型或 object 类型
if (IsClay(resultType) || resultType == typeof(IEnumerable<dynamic?>)) if (IsClay(resultType) || resultType == typeof(IEnumerable<dynamic?>) || resultType == typeof(object))
{ {
return this; return this;
} }
@@ -746,15 +986,23 @@ public partial class Clay
} }
// 检查是否是 IEnumerable<KeyValuePair<string, dynamic?>> 类型且是单一对象 // 检查是否是 IEnumerable<KeyValuePair<string, dynamic?>> 类型且是单一对象
if (resultType == typeof(IEnumerable<KeyValuePair<string, dynamic?>>) && IsObject) if (typeof(IEnumerable<KeyValuePair<string, dynamic?>>).IsAssignableFrom(resultType) && IsObject)
{ {
return AsEnumerateObject(); return resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
? AsEnumerateObject().ToDictionary(u => u.Key, u => u.Value)
: AsEnumerateObject();
} }
// 检查是否是 IEnumerable<KeyValuePair<int, dynamic?>> 类型且是集合或数组 // 检查是否是 IEnumerable<KeyValuePair<int, dynamic?>> 类型且是集合或数组
if (resultType == typeof(IEnumerable<KeyValuePair<int, dynamic?>>) && IsArray) if (typeof(IEnumerable<KeyValuePair<int, dynamic?>>).IsAssignableFrom(resultType) && IsArray)
{ {
return AsEnumerateArray().Select((item, index) => new KeyValuePair<int, dynamic?>(index, item)); // 将流变对象转换为键值对集合
var keyValuePairs =
AsEnumerateArray().Select((item, index) => new KeyValuePair<int, dynamic?>(index, item));
return resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
? keyValuePairs.ToDictionary(u => u.Key, u => u.Value)
: keyValuePairs;
} }
// 检查是否是 IActionResult 类型 // 检查是否是 IActionResult 类型
@@ -926,7 +1174,8 @@ public partial class Clay
/// <returns> /// <returns>
/// <see cref="bool" /> /// <see cref="bool" />
/// </returns> /// </returns>
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());
/// <summary> /// <summary>
/// 按照键升序排序并返回新的 <see cref="Clay" /> /// 按照键升序排序并返回新的 <see cref="Clay" />
@@ -945,7 +1194,7 @@ public partial class Clay
// 初始化升序排序字典 // 初始化升序排序字典
var sorted = var sorted =
new SortedDictionary<string, JsonNode?>(JsonCanvas.AsObject().ToDictionary()); new SortedDictionary<string, JsonNode?>(JsonCanvas.AsObject().ToDictionary(), StringComparer.Ordinal);
return Parse(sorted, options); return Parse(sorted, options);
} }
@@ -969,7 +1218,7 @@ public partial class Clay
// 初始化降序排序字典 // 初始化降序排序字典
var sortedDesc = var sortedDesc =
new SortedDictionary<string, JsonNode?>(Comparer<string>.Create((x, y) => new SortedDictionary<string, JsonNode?>(Comparer<string>.Create((x, y) =>
string.Compare(y, x, StringComparison.InvariantCulture))); string.Compare(y, x, StringComparison.Ordinal)));
// 将 JsonCanvas 转换为 JsonObject 实例 // 将 JsonCanvas 转换为 JsonObject 实例
var jsonObject = JsonCanvas.AsObject(); var jsonObject = JsonCanvas.AsObject();
@@ -1031,6 +1280,64 @@ public partial class Clay
return Rebuilt(Options.Configure(configure)); return Rebuilt(Options.Configure(configure));
} }
/// <summary>
/// 检查字符串是否是 JSON 对象({})或数组([]
/// </summary>
/// <param name="input">字符串</param>
/// <param name="allowTrailingCommas">是否允许末尾多余逗号。默认值为:<c>false</c>。</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
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;
}
}
/// <summary>
/// 将 <see cref="Clay" /> 实例通过转换管道传递并返回新的 <see cref="Clay" />(失败时抛出异常)
/// </summary>
/// <param name="transformer">转换函数</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
/// <exception cref="InvalidOperationException"></exception>
public Clay Pipe(Func<dynamic, dynamic?> transformer) => ExecuteTransformation(transformer, true);
/// <summary>
/// 尝试将 <see cref="Clay" /> 实例通过转换管道传递,失败时返回原始对象
/// </summary>
/// <param name="transformer">转换函数</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
public Clay PipeTry(Func<dynamic, dynamic?> transformer) => ExecuteTransformation(transformer, false);
/// <summary> /// <summary>
/// 单一对象 /// 单一对象
/// </summary> /// </summary>

View File

@@ -0,0 +1,147 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.Shapeless;
/// <summary>
/// 流变对象
/// </summary>
public partial class Clay
{
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override bool Equals(object? obj) => ReferenceEquals(this, obj) || Equals(obj as Clay);
/// <summary>
/// 重载 == 运算符
/// </summary>
/// <param name="left">
/// <see cref="Clay" />
/// </param>
/// <param name="right">
/// <see cref="Clay" />
/// </param>
/// <returns>
/// <see cref="bool" />
/// </returns>
public static bool operator ==(Clay? left, Clay? right) => Equals(left, right);
/// <summary>
/// 重载 != 运算符
/// </summary>
/// <param name="left">
/// <see cref="Clay" />
/// </param>
/// <param name="right">
/// <see cref="Clay" />
/// </param>
/// <returns>
/// <see cref="bool" />
/// </returns>
public static bool operator !=(Clay? left, Clay? right) => !(left == right);
/// <inheritdoc />
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();
}
/// <summary>
/// 检查两个单一对象实例是否相等
/// </summary>
/// <param name="clay1">
/// <see cref="Clay" />
/// </param>
/// <param name="clay2">
/// <see cref="Clay" />
/// </param>
/// <returns>
/// <see cref="bool" />
/// </returns>
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]));
/// <summary>
/// 检查两个集合或数组实例是否相等
/// </summary>
/// <param name="clay1">
/// <see cref="Clay" />
/// </param>
/// <param name="clay2">
/// <see cref="Clay" />
/// </param>
/// <returns>
/// <see cref="bool" />
/// </returns>
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;
}
}

View File

@@ -42,6 +42,10 @@ public partial class Clay
return csharpInvokeMemberBinderType.CreatePropertyGetter(typeArgumentsProperty); return csharpInvokeMemberBinderType.CreatePropertyGetter(typeArgumentsProperty);
}); });
/// <inheritdoc />
/// <remarks>可用于控制序列化时能够被序列化的标识符。</remarks>
public override IEnumerable<string> GetDynamicMemberNames() => Keys.Select(u => u.ToString()!);
/// <inheritdoc /> /// <inheritdoc />
public override bool TryGetMember(GetMemberBinder binder, out object? result) public override bool TryGetMember(GetMemberBinder binder, out object? result)
{ {

View File

@@ -21,7 +21,7 @@ namespace ThingsGateway.Shapeless;
/// <summary> /// <summary>
/// 流变对象 /// 流变对象
/// </summary> /// </summary>
public partial class Clay : DynamicObject, IEnumerable<object?>, IFormattable public partial class Clay : DynamicObject, IEnumerable<object?>, IFormattable, IEquatable<Clay>
{ {
/// <summary> /// <summary>
/// <inheritdoc cref="Clay" /> /// <inheritdoc cref="Clay" />
@@ -535,6 +535,9 @@ public partial class Clay : DynamicObject, IEnumerable<object?>, IFormattable
JsonNode jsonNode => jsonNode.DeepClone(), JsonNode jsonNode => jsonNode.DeepClone(),
// 该操作不会复制自定义委托方法 // 该操作不会复制自定义委托方法
Clay clay => clay.DeepClone(options).JsonCanvas, 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) _ => JsonSerializer.SerializeToNode(obj, options?.JsonSerializerOptions)
}; };
@@ -709,6 +712,60 @@ public partial class Clay : DynamicObject, IEnumerable<object?>, IFormattable
return true; return true;
} }
/// <summary>
/// 执行 <see cref="Clay" /> 实例的核心转换逻辑,支持严格模式和容错模式
/// </summary>
/// <param name="transformer">转换函数</param>
/// <param name="strictMode">
/// 模式开关:
/// - true严格模式失败抛出异常
/// - false容错模式失败返回原对象
/// </param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
/// <exception cref="InvalidOperationException"></exception>
internal Clay ExecuteTransformation(Func<dynamic, dynamic?> 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;
}
/// <summary> /// <summary>
/// 抛出越界的数组索引异常 /// 抛出越界的数组索引异常
/// </summary> /// </summary>

View File

@@ -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;
/// <summary>
/// <see cref="object" /> 转 <see cref="Clay" /> JSON 序列化转换器
/// </summary>
public sealed class ObjectToClayJsonConverter : JsonConverter<object>
{
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}

View File

@@ -0,0 +1,77 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.Shapeless.Extensions;
/// <summary>
/// 流变对象模块拓展类
/// </summary>
public static class ShapelessExtensions
{
/// <summary>
/// 将对象转换为 <see cref="Clay" /> 实例
/// </summary>
/// <param name="obj">
/// <see cref="object" />
/// </param>
/// <param name="options">
/// <see cref="ClayOptions" />
/// </param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
public static Clay ToClay(this object? obj, ClayOptions? options = null) => Clay.Parse(obj, options);
/// <summary>
/// 将对象转换为 <see cref="Clay" /> 实例
/// </summary>
/// <param name="obj">
/// <see cref="object" />
/// </param>
/// <param name="configure">自定义配置委托</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
public static Clay ToClay(this object? obj, Action<ClayOptions> configure) => Clay.Parse(obj, configure);
/// <summary>
/// 将 <see cref="Clay" /> 实例通过转换管道传递并返回新的 <see cref="Clay" />(失败时抛出异常)
/// </summary>
/// <param name="clayTask">
/// <see cref="Task{TResult}" />
/// </param>
/// <param name="transformer">转换函数</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
/// <exception cref="InvalidOperationException"></exception>
public static async Task<Clay?> PipeAsync(this Task<Clay?> clayTask, Func<dynamic, dynamic?> transformer)
{
var clay = await clayTask.ConfigureAwait(false);
return clay?.Pipe(transformer);
}
/// <summary>
/// 尝试将 <see cref="Clay" /> 实例通过转换管道传递,失败时返回原始对象
/// </summary>
/// <param name="clayTask">
/// <see cref="Task{TResult}" />
/// </param>
/// <param name="transformer">returns</param>
/// <returns>
/// <see cref="Clay" />
/// </returns>
public static async Task<Clay?> PipeTryAsync(this Task<Clay?> clayTask, Func<dynamic, dynamic?> transformer)
{
var clay = await clayTask.ConfigureAwait(false);
return clay?.PipeTry(transformer);
}
}

View File

@@ -20,6 +20,17 @@ namespace Microsoft.Extensions.DependencyInjection;
/// </summary> /// </summary>
public static class ShapelessMvcBuilderExtensions public static class ShapelessMvcBuilderExtensions
{ {
/// <summary>
/// 添加 <see cref="Clay" /> 配置
/// </summary>
/// <param name="builder">
/// <see cref="IMvcBuilder" />
/// </param>
/// <returns>
/// <see cref="IMvcBuilder" />
/// </returns>
public static IMvcBuilder AddClayOptions(this IMvcBuilder builder) => builder.AddClayOptions(_ => { });
/// <summary> /// <summary>
/// 添加 <see cref="Clay" /> 配置 /// 添加 <see cref="Clay" /> 配置
/// </summary> /// </summary>

View File

@@ -12,7 +12,7 @@
namespace ThingsGateway.Shapeless; namespace ThingsGateway.Shapeless;
/// <summary> /// <summary>
/// <see cref="Clay" /> 对象事件数 /// <see cref="Clay" /> 对象事件
/// </summary> /// </summary>
public sealed class ClayEventArgs : EventArgs public sealed class ClayEventArgs : EventArgs
{ {

Some files were not shown because too many files have changed in this diff Show More