Compatibility 是多目标框架策略的执行层。本仓库的公共契约长期兼容 net5.0,但 .NET 8/10 引入了大量更安全的 BCL API(ArgumentNullException.ThrowIfNull、Convert.ToHexString、带 CancellationToken 的 HttpContent 重载等),net5 上并不存在。Compatibility 用 #if NET5_0 在 net5 自实现这些缺失 API,在 net8+ 直接委托 BCL,对外暴露统一调用面。
几乎所有包都通过 global using 引入 Compatibility,因此业务代码里看到的 ThrowIfNull、ToHexString、ReadAsStringAsync(content, ct) 全部来自这里。
包信息
| 项 | 值 |
|---|---|
| NuGet ID | Bitzsoft.Integrations.Compatibility |
| 目标框架 | net5.0;net8.0;net10.0 |
| 命名空间 | Bitzsoft.Integrations.Compatibility |
| 引入方式 | 项目级 global using Bitzsoft.Integrations.Compatibility; |
| InternalsVisibleTo | Bitzsoft.Integrations.RequestLogging、Bitzsoft.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 | 值为 null | ArgumentNullException |
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.ToByte | Convert.FromHexString |
ComputeMd5Hash(bytes) | MD5.Create().ComputeHash | MD5.HashData |
ComputeSha1Hash(bytes) | SHA1.Create().ComputeHash | SHA1.HashData |
ComputeSha256Hash(bytes) | SHA256.Create().ComputeHash | SHA256.HashData |
签名计算和摘要场景的高频调用:
// ① 计算请求体 SHA-256 摘要并转小写十六进制(HMAC 签名常见格式)var hash = CompatibilityConvert.ComputeSha256Hash(bodyBytes);var hex = CompatibilityConvert.ToHexString(hash).ToLowerInvariant();net5 分支里 ToHexString 用 StringComparison.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(System、System.Net.Http、System.Threading 等)。引用 Compatibility 的包会在自己的 GlobalUsings.cs 中加上:
global using Bitzsoft.Integrations.Compatibility;这样包内所有文件无需逐个 using,ThrowIfNull、ToHexString 等可直接使用。这也是为什么跨包代码里几乎看不到对 Bitzsoft.Integrations.Compatibility 的显式引用。
何时新增成员
判断是否应新增成员的依据:
- 该 API 在 net5 与 net8+ 行为或签名有差异;
- 多个包会用到它;
- 自实现逻辑足够简单,不会引入新的依赖。
满足这三点就适合放进 Compatibility。厂商特定的复杂逻辑仍应留在厂商包内。