Authorize.Net controller.Execute() 的 HttpUtility 初始化异常
结论
System.TypeInitializationException 表示 AuthorizeNet.Util.HttpUtility 的静态初始化代码执行失败。controller.Execute() 是首次触发该类型加载的位置。这通常不是 Authorize.Net 返回的支付错误,也和 TLS 1.2 没有直接关系。
只看最外层异常无法判断具体原因,需要继续检查 InnerException。在 .NET 8 项目中,常见原因有:
- SDK 所需的配置项缺失或格式错误。也可能是 SDK 仍从旧式
App.config读取配置,而项目只配置了appsettings.json。 System.Configuration.ConfigurationManager等依赖未正确还原,或者依赖版本发生冲突。- SDK 的目标框架或传递依赖与当前运行环境不兼容。
- 发布裁剪、单文件发布或部署目录缺少依赖,造成反射或配置读取失败。
如果问题出在静态构造函数的配置解析或程序集加载过程,降级 SDK、清理项目和强制启用 TLS 都不会解决它。
先取得真正的异常
不要只记录 ex.Message。TypeInitializationException 会将原始异常放在 InnerException 中,而完整的 ex.ToString() 通常已经包含整条异常链。
try
{
controller.Execute();
}
catch (TypeInitializationException ex)
{
Console.Error.WriteLine(ex.ToString());
Exception? current = ex.InnerException;
var level = 1;
while (current != null)
{
Console.Error.WriteLine(
$"InnerException #{level}: {current.GetType().FullName}: {current.Message}");
current = current.InnerException;
level++;
}
throw;
}
排查时主要看最内层异常的类型和消息:
FileNotFoundException、FileLoadException:依赖程序集缺失或版本不匹配。ConfigurationErrorsException:配置文件、配置节或配置值有问题。FormatException:某个字符串配置无法转换成数字或布尔值。NullReferenceException:SDK 没有妥善处理缺失的配置项。TypeLoadException、MissingMethodException:SDK 与实际加载的依赖版本不兼容。
完整堆栈也要保留。如果堆栈中出现具体的配置键、ConfigurationManager.AppSettings 或程序集名称,就可以据此缩小排查范围。
检查 NuGet 依赖
先确认项目最终解析了哪些包,包括传递依赖:
dotnet list package --include-transitive
dotnet restore --force-evaluate
dotnet build
检查以下内容:
- 项目中是否同时存在多个
AuthorizeNet版本。 System.Configuration.ConfigurationManager是否正常解析。- 构建过程中是否出现包降级、程序集冲突或目标框架不兼容警告。
- 部署目录中的 DLL 是否与本地编译时使用的版本一致。
如果最内层异常明确指出缺少 System.Configuration.ConfigurationManager,可以添加显式引用。具体版本应与项目当前的 .NET 8 依赖解析结果一致,不要在没有检查异常和依赖树的情况下随意指定。例如:
<ItemGroup>
<PackageReference Include="AuthorizeNet" Version="2.0.4" />
<PackageReference Include="System.Configuration.ConfigurationManager"
Version="8.0.0" />
</ItemGroup>
这里的 8.0.0 只是 .NET 8 项目中的常见选择,并不适用于所有 AuthorizeNet 依赖组合。最终版本要根据 NuGet 解析结果和最内层异常确定。
区分 App.config 和 appsettings.json
旧式 SDK 可能通过以下 API 读取配置:
System.Configuration.ConfigurationManager.AppSettings["someKey"]
这个 API 不会自动读取 ASP.NET Core 的 appsettings.json:
{
"SomeKey": "value"
}
如果异常堆栈表明 HttpUtility 正在读取某个 appSettings 键,就要按该 SDK 实际使用的键名配置 App.config,或者升级到不依赖旧式配置读取方式的 SDK 版本。配置结构可以参考:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="httpConnectionTimeout" value="30000" />
</appSettings>
</configuration>
httpConnectionTimeout 只是一个可能出现的配置示例。只有异常信息或对应版本的 SDK 源码确认读取了这个键,才需要添加。不要根据猜测不断增加配置项。
还要检查输出目录中是否生成了对应的 .dll.config 文件,以及文件里的值是否仍然有效。数字配置要使用纯整数,不能写成 "30s"、空字符串或带单位的值。
修正示例代码中的认证参数
当前代码还存在一个独立问题。方法接收的 ApiLoginID 和 ApiTransactionKey 被写入 ApiOperationBase,实际请求却使用了两个空的局部变量:
var apiLoginId = "";
var transactionKey = "";
后续认证对象也使用了这两个变量:
var merchantAuth = new merchantAuthenticationType
{
name = apiLoginId,
ItemElementName = ItemChoiceType.transactionKey,
Item = transactionKey
};
即使解决了 HttpUtility 初始化异常,这段代码仍会用空凭据发送请求。Authorize.Net 通常会返回认证失败,而不是 TypeInitializationException。
可以只保留一套认证信息:
public static ANetApiResponse Run(
string apiLoginId,
string transactionKey,
decimal amount)
{
ApiOperationBase<ANetApiRequest, ANetApiResponse>.RunEnvironment =
AuthorizeNet.Environment.SANDBOX;
var merchantAuth = new merchantAuthenticationType
{
name = apiLoginId,
ItemElementName = ItemChoiceType.transactionKey,
Item = transactionKey
};
var transactionRequest = new transactionRequestType
{
transactionType =
transactionTypeEnum.authCaptureTransaction.ToString(),
amount = amount
};
var request = new getHostedPaymentPageRequest
{
merchantAuthentication = merchantAuth,
transactionRequest = transactionRequest,
hostedPaymentSettings =
[
new settingType
{
settingName = "hostedPaymentReturnOptions",
settingValue =
"""
{
"showReceipt": false,
"url": "https://yourwebsite.com/payment-success",
"urlText": "Continue"
}
"""
}
]
};
var controller = new getHostedPaymentPageController(request);
try
{
controller.Execute();
}
catch (TypeInitializationException ex)
{
Console.Error.WriteLine(ex.ToString());
throw;
}
return controller.GetApiResponse();
}
hostedPaymentSettings 支持的名称和 JSON 结构,应以当前 Authorize.Net Accept Hosted 接口及项目使用的 SDK 版本为准。如果初始化问题解决后出现接口校验错误,再单独检查这些设置。
原代码中还有几处需要删除或修正:
RunEnvironment和MerchantAuthentication被重复赋值。amount参数没有使用,交易金额被固定为50.00m。creditCard、billingAddress、paymentType和lineItems创建后没有加入请求。- 请求使用的是局部空凭据,与
ApiOperationBase中设置的凭据不一致。
这些问题不会导致 HttpUtility 静态初始化失败,但会影响后续请求。
如果只在发布后出现
如果调试运行正常,发布后才报错,可以暂时关闭裁剪和单文件发布进行对比:
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<PublishSingleFile>false</PublishSingleFile>
</PropertyGroup>
旧式 SDK 可能依赖反射、配置文件或运行时动态加载,相关类型或程序集可能在裁剪时被移除。如果关闭这些选项后异常消失,就需要保留对应程序集,或者不要对该应用启用裁剪。
同时确认部署环境的目标架构和 .NET 8 Runtime 是否正确,并检查发布目录中有没有异常所指向的依赖文件。
TLS 设置为什么没有效果
ServicePointManager.SecurityProtocol 控制网络连接使用的协议,而静态类型初始化通常发生在 HTTP 请求真正发出之前。如果 HttpUtility 在读取配置或加载程序集时就失败了,程序尚未进入 TLS 握手阶段,下面的设置自然不会改变结果:
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
在现代 .NET 中,HTTP 栈通常会根据操作系统和运行时配置协商 TLS。只有最内层异常明确指向连接、证书或 TLS 握手问题,才需要继续排查网络协议。
建议的排查顺序
- 输出完整的
TypeInitializationException.ToString()和所有InnerException。 - 根据最内层异常判断问题属于配置错误、程序集缺失、版本冲突还是发布裁剪。
- 使用
dotnet list package --include-transitive检查项目实际解析的依赖。 - 如果 SDK 使用
ConfigurationManager.AppSettings,确认配置位于它可以读取的配置文件中。 - 清理重复配置,确保请求使用方法传入的真实 Sandbox 凭据。
- 在关闭裁剪和单文件发布的普通 Debug 环境中创建最小复现项目。
- 修复后重新启动进程。静态构造函数失败后,CLR 会在当前进程中继续将该类型视为初始化失败,重复调用
Execute()通常无法恢复。
判断原因时应以最内层异常为准。缺少 InnerException 的具体内容,就无法确定问题来自哪个配置键或哪个依赖版本。
备注:内容仅供参考。
