Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Reference

Compatibility 兼容层

抹平 net5/8/10 BCL API 差异的兼容层——参数校验、HTTP 内容读取、十六进制与哈希、请求体抑制。

Last updated

Compatibility 是多目标框架策略的执行层。本仓库的公共契约长期兼容 net5.0,但 .NET 8/10 引入了大量更安全的 BCL API(ArgumentNullException.ThrowIfNullConvert.ToHexString、带 CancellationTokenHttpContent 重载等),net5 上并不存在。Compatibility 用 #if NET5_0 在 net5 自实现这些缺失 API,在 net8+ 直接委托 BCL,对外暴露统一调用面。

几乎所有包都通过 global using 引入 Compatibility,因此业务代码里看到的 ThrowIfNullToHexStringReadAsStringAsync(content, ct) 全部来自这里。

包信息

NuGet IDBitzsoft.Integrations.Compatibility
目标框架net5.0;net8.0;net10.0
命名空间Bitzsoft.Integrations.Compatibility
引入方式项目级 global using Bitzsoft.Integrations.Compatibility;
InternalsVisibleToBitzsoft.Integrations.RequestLoggingBitzsoft.Integrations.Rest

设计模式:#if NET5_0

每个成员都用同一种条件编译模式。net5 走自实现分支,net8+ 走 BCL 委托分支,两者语义一致:

public static class CompatibilityArgument
{
public static void ThrowIfNull(
object? value,
[CallerArgumentExpression("value")] string? paramName = null)
{
#if NET5_0
// ① net5 上自实现:BCL 还没有 ArgumentNullException.ThrowIfNull
if (value is null)
throw new ArgumentNullException(paramName ?? nameof(value));
#else
// ② net8+ 直接委托 BCL,零额外开销
ArgumentNullException.ThrowIfNull(value, paramName);
#endif
}
}

[CallerArgumentExpression] 特性让 paramName 自动取实参表达式名,业务侧写 ThrowIfNull(order) 就能得到 "order" 作为参数名,无需手写 nameof。这个特性在 net5 的编译器层面已支持(由 Roslyn 决定),与运行时无关。

成员一览

CompatibilityArgument

参数校验工具,对应 net8+ 的 ArgumentException.ThrowIf* 系列。

方法触发条件抛出异常
ThrowIfNull值为 nullArgumentNullException
ThrowIfNullOrEmpty字符串为 null 或空ArgumentNullException / ArgumentException
ThrowIfNullOrWhiteSpace字符串为 null、空或仅空白ArgumentNullException / ArgumentException
public sealed class PaymentOptions
{
public PaymentOptions(string appId, string privateKey)
{
CompatibilityArgument.ThrowIfNullOrEmpty(appId); // ① 参数名自动推断为 appId
CompatibilityArgument.ThrowIfNullOrWhiteSpace(privateKey);
AppId = appId;
PrivateKey = privateKey;
}
public string AppId { get; }
public string PrivateKey { get; }
}

CompatibilityHttpContent

封装 HttpContent 在不同 TFM 上的取消令牌重载差异。net5 的 ReadAsStringAsync() 不接受 CancellationToken,net8+ 有带 CT 的重载。

方法说明
ReadAsStringAsync(content, ct)读取为字符串
ReadAsByteArrayAsync(content, ct)读取为字节数组,不施加大小上限
ReadAsStreamAsync(content, ct)打开内容流
CreateFormUrlEncodedContent(formData)创建表单 URL 编码内容

net5 分支会先调用 cancellationToken.ThrowIfCancellationRequested() 再委托无 CT 重载,尽力保证取消语义:

public static Task<string> ReadAsStringAsync(
HttpContent content,
CancellationToken cancellationToken = default)
{
CompatibilityArgument.ThrowIfNull(content);
#if NET5_0
cancellationToken.ThrowIfCancellationRequested(); // ① net5 只能在调用前检查一次
return content.ReadAsStringAsync();
#else
return content.ReadAsStringAsync(cancellationToken); // ② net8+ 全程可取消
#endif
}

CompatibilityConvert

十六进制转换与哈希计算,对应 net8+ 的 Convert.ToHexString 和静态 HashData API。

方法net5 实现net8+ 委托
ToHexString(bytes)BitConverter.ToString(...).Replace("-", "")Convert.ToHexString
FromHexString(hex)手动逐字节 Convert.ToByteConvert.FromHexString
ComputeMd5Hash(bytes)MD5.Create().ComputeHashMD5.HashData
ComputeSha1Hash(bytes)SHA1.Create().ComputeHashSHA1.HashData
ComputeSha256Hash(bytes)SHA256.Create().ComputeHashSHA256.HashData

签名计算和摘要场景的高频调用:

// ① 计算请求体 SHA-256 摘要并转小写十六进制(HMAC 签名常见格式)
var hash = CompatibilityConvert.ComputeSha256Hash(bodyBytes);
var hex = CompatibilityConvert.ToHexString(hash).ToLowerInvariant();

net5 分支里 ToHexStringStringComparison.Ordinal 做替换,避免因文化差异导致的大小写或连字符处理异常。

CompatibilityHttpRequestMessage

跨 TFM 的请求诊断元数据,用于抑制某次请求的正文日志记录。这个类型是 internal,仅对 RequestLogging 和 Rest 可见(通过 InternalsVisibleTo)。

方法说明
SuppressRequestAndResponseBodies(request)标记请求体与响应体不进审计日志
AreRequestAndResponseBodiesSuppressed(request)检查是否已标记抑制
// ② RequestLogHandler 读取该标记后跳过正文捕获
internal static void SuppressRequestAndResponseBodies(HttpRequestMessage request)
{
CompatibilityArgument.ThrowIfNull(request);
request.Options.Set(SuppressBodiesKey, true);
}

业务侧不直接调用,而是通过 RestRequest.SuppressRequestAndResponseBodiesFromLogging 或厂商请求的等价属性间接设置。涉及凭据刷新、token 交换等含敏感数据的请求会用它避免正文进日志。

InternalsVisibleTo

Compatibility 在 .csproj 中声明了对两个包的内部可见性:

<ItemGroup>
<InternalsVisibleTo Include="Bitzsoft.Integrations.RequestLogging" />
<InternalsVisibleTo Include="Bitzsoft.Integrations.Rest" />
</ItemGroup>

CompatibilityHttpRequestMessage 之所以是 internal,是因为它的 SuppressRequestAndResponseBodies 只服务于 RequestLogging 的正文捕获逻辑和 Rest 的请求构建逻辑,不属于公共 API 面。宿主和厂商包不应直接调用。

global using 引入

Compatibility 自身不发布 GlobalUsings.cs,而是依赖 .NET SDK 默认生成的隐式 global using(SystemSystem.Net.HttpSystem.Threading 等)。引用 Compatibility 的包会在自己的 GlobalUsings.cs 中加上:

global using Bitzsoft.Integrations.Compatibility;

这样包内所有文件无需逐个 usingThrowIfNullToHexString 等可直接使用。这也是为什么跨包代码里几乎看不到对 Bitzsoft.Integrations.Compatibility 的显式引用。

何时新增成员

判断是否应新增成员的依据:

  1. 该 API 在 net5 与 net8+ 行为或签名有差异;
  2. 多个包会用到它;
  3. 自实现逻辑足够简单,不会引入新的依赖。

满足这三点就适合放进 Compatibility。厂商特定的复杂逻辑仍应留在厂商包内。

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%