
.NET Exception 序列化问题深度解析:为什么 HelpURL 属性会导致反序列化失败?
一、问题现象
今天早上在处理客户反馈的问题时,遇到了一个令人困惑的异常:
2026-05-06 09:40:07.116 WARN [274] - Member 'HelpURL' was not found.
System.Runtime.Serialization.SerializationException: Member 'HelpURL' was not found.
at System.Runtime.Serialization.SerializationInfo.GetElement(String name, Type& foundType)
at System.Runtime.Serialization.SerializationInfo.GetString(String name)
at System.Exception..ctor(SerializationInfo info, StreamingContext context)
at lambda_method(Closure , Object[] )
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateISerializable(JsonReader reader, JsonISerializableContract contract, JsonProperty member, String id)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
.....
项目代码比较复杂,我创建了一个简单项目,用于演示碰见的问题:
完整测试代码
namespace ConsoleApp5
{
internal class Program
{
static void Main(string[] args)
{
try
{
throw new Exception("闲来无事,抛个异常玩玩~");
}
catch (Exception ex)
{
UseOriginalException(ex);
UseCustomWithoutSerializableException(ex);
UseCustomWithSerializableException(ex);
}
Console.ReadLine();
}
/// <summary>
/// 使用原生异常类
/// </summary>
/// <param name="ex"></param>
static void UseOriginalException(Exception ex)
{
try
{
var str = JsonConvert.SerializeObject(ex);
var info2 = JsonConvert.DeserializeObject<Exception>(str);
Console.WriteLine("【UseOriginalException】没有发生错误");
}
catch (Exception ex2)
{
Console.WriteLine("【UseOriginalException】发生错误:" + ex2.Message);
}
}
/// <summary>
/// 使用不带[Serializable]特性的自定义异常类
/// </summary>
/// <param name="ex"></param>
static void UseCustomWithoutSerializableException(Exception ex)
{
try
{
CustomWithoutSerializableException custom = new(ex.Message, ex);
var str = JsonConvert.SerializeObject(custom);
var info2 = JsonConvert.DeserializeObject<Exception>(str);
Console.WriteLine("【UseCustomWithoutSerializableException】没有发生错误");
}
catch (Exception ex2)
{
Console.WriteLine("【UseCustomWithoutSerializableException】发生错误:" + ex2.Message);
}
}
/// <summary>
/// 使用带[Serializable]特性的自定义异常类
/// </summary>
/// <param name="ex"></param>
static void UseCustomWithSerializableException(Exception ex)
{
try
{
CustomWithSerializableException custom = new(ex.Message, ex);
var str = JsonConvert.SerializeObject(custom);
var info2 = JsonConvert.DeserializeObject<Exception>(str);
Console.WriteLine("【UseCustomWithSerializableException】没有发生错误");
}
catch (Exception ex2)
{
Console.WriteLine("【UseCustomWithSerializableException】发生错误:" + ex2.Message);
}
}
/// <summary>
/// 不带[Serializable]特性的自定义异常类
/// </summary>
public class CustomWithoutSerializableException : Exception
{
public CustomWithoutSerializableException() : base() { }
public CustomWithoutSerializableException(string message) : base(message) { }
public CustomWithoutSerializableException(string message, Exception innerException) : base(message, innerException) { }
}
/// <summary>
/// 带[Serializable]特性的自定义异常类
/// </summary>
[Serializable]
public class CustomWithSerializableException : Exception
{
public CustomWithSerializableException() : base() { }
public CustomWithSerializableException(string message) : base(message) { }
public CustomWithSerializableException(string message, Exception innerException) : base(message, innerException) { }
}
}
运行结果
【UseOriginalException】没有发生错误
【UseCustomWithoutSerializableException】发生错误:Member 'HelpURL' was not found.
【UseCustomWithSerializableException】没有发生错误

问题分析
奇怪的现象:
- 原生
Exception序列化/反序列化成功 - 不带
[Serializable]的自定义异常反序列化失败,报错:Member 'HelpURL' was not found. - 带有
[Serializable]的自定义异常序列化/反序列化成功
更奇怪的是: 错误提示说找不到 HelpURL 成员,但实际上 Exception 类并没有 HelpURL 这个属性!Exception 类只有 HelpLink 属性。
二、差异对比与本质分析
2.1 两个自定义异常的唯一区别
对比 CustomWithoutSerializableException 和 CustomWithSerializableException,我们发现:
- 唯一的区别:
CustomWithSerializableException多了一个[Serializable]特性 - 代码完全相同:构造函数、继承结构完全一致
这说明 [Serializable] 特性在序列化过程中扮演了关键角色。
2.2 为什么会有这种差异?
Newtonsoft.Json 的序列化行为
当 Newtonsoft.Json 序列化一个对象时:
-
有
[Serializable]特性的类:- 被识别为”可序列化类型”
- Newtonsoft.Json 会尝试序列化所有公共属性和字段
- 对于继承的属性,会遵循基类的序列化行为
-
没有
[Serializable]特性的类:- 被视为普通 POCO 对象
- Newtonsoft.Json 序列化所有可访问的属性
- 包括从基类继承的所有公共属性
关键问题:Exception 类的特殊实现
Exception 类是一个非常特殊的基类:
- 它本身标记了
[Serializable]特性 - 它实现了
ISerializable接口 - 它有一些内部的序列化逻辑
当我们创建一个不带 [Serializable] 的自定义异常时:
CustomWithoutSerializableException custom = new(ex.Message, ex);
var str = JsonConvert.SerializeObject(custom);
序列化过程中,Newtonsoft.Json 会:
- 扫描
CustomWithoutSerializableException的所有属性 - 同时扫描从
Exception继承的所有属性 - 序列化时会包含一些 Exception 内部的特殊字段
反序列化时:
var info2 = JsonConvert.DeserializeObject<Exception>(str);
- 目标类型是
Exception - JSON 中包含了一些来自子类序列化的额外字段
Exception类在反序列化时期望某些特定的序列化格式- 当格式不匹配时,就会报错找不到
HelpURL成员
三、深挖 Exception 类的序列化实现原理
3.1 Exception 类的源码分析
Exception 类在 .NET 中实现了 ISerializable 接口,这是 .NET Framework 时代的二进制序列化机制。
源码参考:
- .NET Runtime 官方源码:Exception.cs
- 本节分析基于 .NET 8.0 的实现
核心字段定义(.NET 源码)
首先,让我们看看 Exception 类中定义的关键私有字段:
// 摘自 .NET Runtime 源码
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Exception.cs
[Serializable]
public class Exception : ISerializable
{
private string? _message;
private IDictionary? _data;
private Exception? _innerException;
private string? _helpURL; // ?? 关键字段:内部使用 _helpURL
private object? _stackTrace;
private string? _stackTraceString;
private string? _remoteStackTraceString;
private string? _source;
private object? _watsonBuckets;
// ... 其他字段
}
HelpLink 属性与 _helpURL 字段的关系
// 摘自 .NET Runtime 源码
public virtual string? HelpLink
{
get => _helpURL; // 读取私有字段 _helpURL
set => _helpURL = value; // 设置私有字段 _helpURL
}
关键发现: 公共属性 HelpLink 只是 _helpURL 私有字段的包装器!
序列化构造函数(反序列化入口)
// 摘自 .NET Runtime 源码
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Exception.cs
[Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")]
protected Exception(SerializationInfo info, StreamingContext context)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
_message = info.GetString("Message")!;
_data = (IDictionary?)(info.GetValueNoThrow("Data", typeof(IDictionary)));
_innerException = (Exception?)(info.GetValueNoThrow("InnerException", typeof(Exception)));
_helpURL = info.GetString("HelpURL")!; // 关键:期望 JSON 中有 "HelpURL" 键
_stackTraceString = info.GetString("StackTraceString")!;
_remoteStackTraceString = info.GetString("RemoteStackTraceString")!;
_source = info.GetString("Source")!;
// ... 其他字段的反序列化
}
关键点分析:
- 这个构造函数在反序列化时被调用
- 它明确要求从
SerializationInfo中获取"HelpURL"键(不是"HelpLink") - 如果 JSON 中没有
"HelpURL"键,就会抛出 “Member ‘HelpURL’ was not found” 异常
GetObjectData 方法(序列化出口)
// 摘自 .NET Runtime 源码
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Exception.cs
[Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
string className = GetClassName();
info.AddValue("ClassName", className, typeof(string)); // 类名
info.AddValue("Message", _message, typeof(string)); // 消息
info.AddValue("Data", _data, typeof(IDictionary)); // 数据字典
info.AddValue("InnerException", _innerException, typeof(Exception)); // 内部异常
info.AddValue("HelpURL", _helpURL, typeof(string)); // 使用 "HelpURL" 键名
info.AddValue("StackTraceString", _stackTraceString, typeof(string));
info.AddValue("RemoteStackTraceString", _remoteStackTraceString, typeof(string));
info.AddValue("RemoteStackIndex", 0, typeof(int));
info.AddValue("ExceptionMethod", null, typeof(string));
info.AddValue("HResult", HResult);
info.AddValue("Source", _source, typeof(string));
info.AddValue("WatsonBuckets", _watsonBuckets, typeof(byte[]));
}
private string GetClassName() => GetType().ToString();
关键发现:
GetObjectData方法控制对象如何被序列化- 它将
_helpURL私有字段序列化为"HelpURL"键(不是"HelpLink") - 这个键名与反序列化构造函数期望的键名完全一致
- 方法被标记为
[Obsolete],说明这是为了向后兼容保留的旧 API
3.2 完整的序列化数据结构
通过查看 GetObjectData 方法,我们可以看到 Exception 序列化时包含的所有字段:
| 序列化键名 | 对应字段/属性 | 类型 | 说明 |
|---|---|---|---|
ClassName |
GetType().ToString() |
string | 异常类的完整类名 |
Message |
_message |
string | 异常消息 |
Data |
_data |
IDictionary | 附加数据字典 |
InnerException |
_innerException |
Exception | 内部异常 |
HelpURL |
_helpURL |
string | 帮助链接(注意键名) |
StackTraceString |
_stackTraceString |
string | 堆栈跟踪字符串 |
RemoteStackTraceString |
_remoteStackTraceString |
string | 远程堆栈跟踪 |
RemoteStackIndex |
固定值 0 | int | 远程堆栈索引 |
ExceptionMethod |
null | string | 异常方法(已废弃) |
HResult |
HResult 属性 |
int | 错误代码 |
Source |
_source |
string | 错误源 |
WatsonBuckets |
_watsonBuckets |
byte[] | Watson 错误报告数据 |
核心矛盾点:
- 公共 API 使用:
HelpLink属性 - 内部字段名:
_helpURL - 序列化键名:
HelpURL(既不是属性名,也不完全是字段名)
3.3 为什么 .NET 设计者选择 “HelpURL” 而不是 “HelpLink”?
这是历史遗留问题:
- 早期设计:在 .NET Framework 1.0 时代,可能最初考虑使用 “HelpURL” 作为公共 API 名称
- 后期调整:出于某种原因(可能是命名规范),公共属性改为
HelpLink - 向后兼容:序列化格式一旦确定,就不能轻易更改,否则会破坏已序列化数据的兼容性
- 结果:形成了”公共属性名” ≠ “序列化键名”的情况
类似的设计在 .NET 中很常见:为了保持二进制序列化的兼容性,很多类的序列化键名都与当前的公共 API 不完全一致。
3.4 ISerializable 接口的作用机制
ISerializable 接口的双向契约:
public interface ISerializable
{
// 序列化时调用:将对象状态写入 SerializationInfo
void GetObjectData(SerializationInfo info, StreamingContext context);
}
// 反序列化时调用:通过特殊构造函数从 SerializationInfo 还原对象
protected Constructor(SerializationInfo info, StreamingContext context);
工作流程:
序列化(对象 → JSON):
↓
Newtonsoft.Json 检测到 [Serializable] + ISerializable
↓
调用 GetObjectData(info, context)
↓
对象将状态写入 SerializationInfo
↓
SerializationInfo 转换为 JSON
反序列化(JSON → 对象):
↓
Newtonsoft.Json 检测到目标类型实现 ISerializable
↓
将 JSON 转换为 SerializationInfo
↓
调用特殊构造函数 Exception(info, context)
↓
从 SerializationInfo 读取状态还原对象
3.5 Newtonsoft.Json 对 ISerializable 的特殊处理
Newtonsoft.Json 对实现了 ISerializable 的类型有特殊逻辑:
-
检测条件:
- 类标记了
[Serializable]特性 - 类实现了
ISerializable接口
- 类标记了
-
序列化行为:
- 不使用反射序列化公共属性
- 调用
GetObjectData方法获取序列化数据 - 使用
SerializationInfo中的键值对生成 JSON
-
反序列化行为:
- 不使用参数化构造函数或属性注入
- 将 JSON 转换为
SerializationInfo - 调用反序列化构造函数
(SerializationInfo, StreamingContext)
这就是为什么:
- 带
[Serializable]的自定义异常:使用ISerializable路径,JSON 包含 “HelpURL” - 不带
[Serializable]的自定义异常:使用普通属性序列化,JSON 包含 “HelpLink”
3.6 序列化流程对比
场景1:原生 Exception(成功)?
1. 序列化阶段:
Exception 对象
↓
检测到 [Serializable] + ISerializable
↓
调用 GetObjectData 方法
↓
写入 SerializationInfo:
- "ClassName": "System.Exception"
- "Message": "闲来无事,抛个异常玩玩~"
- "HelpURL": null ← 使用 HelpURL 键名
- "InnerException": null
- "StackTraceString": "..."
- 其他字段...
↓
生成 JSON: {"ClassName":"System.Exception", "Message":"...", "HelpURL":null, ...}
2. 反序列化阶段:
JSON 字符串
↓
目标类型: Exception
↓
检测到 Exception 实现 ISerializable
↓
JSON 转换为 SerializationInfo
↓
调用 Exception(SerializationInfo, StreamingContext) 构造函数
↓
读取 info.GetString("HelpURL") ← JSON 中存在此键
↓
成功创建 Exception 对象 ?
场景2:CustomWithoutSerializableException(失败)?
1. 序列化阶段:
CustomWithoutSerializableException 对象
↓
没有 [Serializable] 特性
↓
作为普通 POCO 对象处理
↓
反射获取所有公共属性:
- Message (继承自 Exception)
- HelpLink (继承自 Exception) ← 注意:属性名是 HelpLink
- StackTrace (继承自 Exception)
- InnerException (继承自 Exception)
- 其他公共属性...
↓
生成 JSON: {"Message":"...", "HelpLink":null, "StackTrace":"...", ...}
↑
键名是 HelpLink,不是 HelpURL!
2. 反序列化阶段:
JSON 字符串
↓
目标类型: Exception (基类)
↓
检测到 Exception 实现 ISerializable
↓
JSON 转换为 SerializationInfo
↓
调用 Exception(SerializationInfo, StreamingContext) 构造函数
↓
尝试读取 info.GetString("HelpURL") ← JSON 中没有此键,只有 "HelpLink"
↓
抛出异常:Member 'HelpURL' was not found
核心问题:
- 序列化用的是
HelpLink属性名 - 反序列化期望的是
HelpURL键名 - 键名不匹配!
场景3:CustomWithSerializableException(成功)?
1. 序列化阶段:
CustomWithSerializableException 对象
↓
检测到 [Serializable] 特性
↓
继承自 Exception(Exception 实现了 ISerializable)
↓
调用继承的 GetObjectData 方法
↓
写入 SerializationInfo:
- "ClassName": "CustomWithSerializableException"
- "Message": "闲来无事,抛个异常玩玩~"
- "HelpURL": null ← 使用 HelpURL 键名
- "InnerException": Exception {...}
- 其他字段...
↓
生成 JSON: {"ClassName":"...", "Message":"...", "HelpURL":null, ...}
2. 反序列化阶段:
JSON 字符串
↓
目标类型: Exception
↓
检测到 Exception 实现 ISerializable
↓
JSON 转换为 SerializationInfo
↓
调用 Exception(SerializationInfo, StreamingContext) 构造函数
↓
读取 info.GetString("HelpURL") ← JSON 中存在此键
↓
成功创建对象
3.7 验证:实际的 JSON 输出差异
让我们通过实际序列化输出来验证上述分析:
原生 Exception 的 JSON(使用 ISerializable):
{
"ClassName": "System.Exception",
"Message": "闲来无事,抛个异常玩玩~",
"Data": null,
"InnerException": null,
"HelpURL": null, // ← 注意:HelpURL
"StackTraceString": "...",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": null,
"HResult": -2146233088,
"Source": null
}
CustomWithoutSerializableException 的 JSON(普通属性序列化):
{
"Message": "闲来无事,抛个异常玩玩~",
"Data": null,
"InnerException": {
"Message": "闲来无事,抛个异常玩玩~",
"Data": null,
"InnerException": null,
"HelpLink": null, // ← 注意:HelpLink(不是 HelpURL)
"Source": null,
"HResult": -2146233088,
"StackTrace": "..."
},
"TargetSite": null,
"HelpLink": null, // ← 注意:HelpLink(不是 HelpURL)
"Source": null,
"HResult": -2146233088,
"StackTrace": "..."
}
关键差异:
- ISerializable 方式:
"HelpURL": null - 普通属性方式:
"HelpLink": null
当用第二个 JSON 反序列化为 Exception 时,Exception 的反序列化构造函数会查找 “HelpURL” 键,但找到的是 “HelpLink”,导致异常!
3.8 如何查看 .NET 源码
如果你想亲自验证以上分析,可以通过以下方式查看 .NET 的实际源码:
方式1:GitHub 在线查看(推荐)
访问 .NET Runtime 官方仓库:
- Exception.cs 主文件:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Exception.cs
关键代码位置:
- _helpURL 字段定义:约第 54 行
- HelpLink 属性:约第 123-127 行
- GetObjectData 方法:约第 205-225 行
- 反序列化构造函数:约第 157-180 行
方式2:Visual Studio 中”转到定义”
- 在 Visual Studio 中,右键点击
Exception类名 - 选择 “转到定义”(F12)
- Visual Studio 会显示从元数据反编译的代码
方式3:使用 ILSpy 或 dnSpy 反编译工具
- 打开
System.Private.CoreLib.dll - 导航到
System.Exception类 - 查看完整实现
方式4:在线 .NET 源码浏览器
- Source.dot.net:https://source.dot.net/
- 搜索
System.Exception即可查看跨版本的源码
关键源码片段总结
通过查看源码,你可以亲眼确认:
-
字段定义:
// 第 54 行附近 private string? _helpURL; -
属性映射:
// 第 123-127 行附近 public virtual string? HelpLink { get => _helpURL; set => _helpURL = value; } -
序列化键名:
// GetObjectData 方法中,第 218 行附近 info.AddValue("HelpURL", _helpURL, typeof(string)); -
反序列化读取:
// 反序列化构造函数中,第 169 行附近 _helpURL = info.GetString("HelpURL")!;
验证结论:
- 私有字段确实叫
_helpURL - 序列化和反序列化都使用
"HelpURL"键名 - 公共属性名是
HelpLink(与序列化键名不同) HelpLink只是_helpURL的包装器
3.9 完整的数据流向图
下图展示了 HelpLink/HelpURL 在不同场景下的完整流向:
┌─────────────────────────────────────────────────────────────────────────┐
│ 场景对比:HelpLink vs HelpURL │
└─────────────────────────────────────────────────────────────────────────┘
?? 场景 1 & 3:使用 ISerializable(成功)
┌─────────────────┐
│ Exception 对象 │
│ _helpURL = null │ ← 私有字段
│ HelpLink 属性 │ ← 公共 API(读写 _helpURL)
└────────┬────────┘
│ 标记了 [Serializable]
↓
┌──────────────┐
│ GetObjectData │ ← ISerializable 接口方法
└──────┬───────┘
│ info.AddValue("HelpURL", _helpURL, ...)
↓
┌─────────┐
│ JSON │ {"ClassName":"...", "HelpURL":null, ...}
└────┬────┘ ↑
│ 注意:键名是 "HelpURL"
↓
┌──────────────────────┐
│ 反序列化构造函数 │ Exception(SerializationInfo, StreamingContext)
└──────┬───────────────┘
│ _helpURL = info.GetString("HelpURL")
│ ↑
│ 在 JSON 中找到了!
↓
┌─────────────────┐
│ Exception 对象 │
│ _helpURL = null │
└─────────────────┘
?? 场景 2:不使用 ISerializable(失败)
┌──────────────────────────────┐
│ CustomWithoutSerializable... │
│ (继承 Exception) │
│ 没有 [Serializable] 特性 │
└────────┬─────────────────────┘
│ Newtonsoft.Json 反射获取所有公共属性
↓
┌───────────────┐
│ 属性序列化 │
└──────┬────────┘
│ 序列化 HelpLink 属性(不是 _helpURL 字段)
↓
┌─────────┐
│ JSON │ {"Message":"...", "HelpLink":null, ...}
└────┬────┘ ↑
│ 注意:键名是 "HelpLink"
↓
┌──────────────────────┐
│ 反序列化为 Exception │ ← 目标类型是基类 Exception
└──────┬───────────────┘
│ Exception 实现了 ISerializable
│ 调用反序列化构造函数
↓
┌──────────────────────┐
│ Exception(info, ctx) │
└──────┬───────────────┘
│ _helpURL = info.GetString("HelpURL")
│ ↑
│ 在 JSON 中找不到!?
│ (JSON 中只有 "HelpLink")
↓
异常!
Member 'HelpURL' was not found.
核心问题总结:
| 方面 | 有 [Serializable] | 无 [Serializable] |
|---|---|---|
| 序列化方式 | ISerializable.GetObjectData | 反射公共属性 |
| JSON 键名 | HelpURL(来自 GetObjectData) |
HelpLink(属性名) |
| 反序列化方式 | ISerializable 构造函数 | ISerializable 构造函数 |
| 期望的键名 | HelpURL |
HelpURL |
| 实际的键名 | HelpURL ? |
HelpLink ? |
| 结果 | 成功 | 失败 |
四、解决方案
方案1:添加 [Serializable] 特性(推荐)
这是最简单、最符合 .NET 设计规范的方案。
[Serializable]
public class CustomException : Exception
{
public CustomException() : base() { }
public CustomException(string message) : base(message) { }
public CustomException(string message, Exception innerException)
: base(message, innerException) { }
// 如果有自定义属性,需要实现 ISerializable
protected CustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
// 反序列化自定义属性
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
// 序列化自定义属性
}
}
优点:
- ? 符合 .NET 异常设计最佳实践
- ? 支持所有序列化场景(二进制、JSON、XML)
- ? 与框架行为一致
缺点:
- ?? 在 .NET Core/5+中,
[Serializable]主要用于兼容性,实际二进制序列化已不推荐
方案2:配置 Newtonsoft.Json 序列化设置
通过自定义序列化设置,避免 ISerializable 的特殊处理。
var settings = new JsonSerializerSettings
{
// 忽略 ISerializable 接口,作为普通对象处理
ContractResolver = new DefaultContractResolver
{
IgnoreSerializableInterface = true
}
};
var str = JsonConvert.SerializeObject(custom, settings);
var info2 = JsonConvert.DeserializeObject<Exception>(str, settings);
优点:
- ? 不需要修改异常类定义
- ? 更符合 JSON 序列化的语义
缺点:
- 需要在所有序列化/反序列化的地方统一使用相同设置
- 可能丢失某些 Exception 内部状态(如原始堆栈跟踪)
方案3:使用 System.Text.Json(.NET Core 3.0+)
从 .NET Core 3.0 开始,微软推出了新的 JSON 序列化库,不依赖 ISerializable。
using System.Text.Json;
var str = JsonSerializer.Serialize(custom);
var info2 = JsonSerializer.Deserialize<Exception>(str);
注意: System.Text.Json 默认不支持 Exception 的序列化,需要自定义转换器:
public class ExceptionConverter : JsonConverter<Exception>
{
public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var doc = JsonDocument.ParseValue(ref reader);
var message = doc.RootElement.GetProperty("Message").GetString();
return new Exception(message);
}
public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("Message", value.Message);
writer.WriteString("StackTrace", value.StackTrace);
writer.WriteString("HelpLink", value.HelpLink);
writer.WriteEndObject();
}
}
// 使用
var options = new JsonSerializerOptions();
options.Converters.Add(new ExceptionConverter());
var str = JsonSerializer.Serialize(custom, options);
var info2 = JsonSerializer.Deserialize<Exception>(str, options);
优点:
- 更现代的 API
- 性能更好
- 不依赖过时的 ISerializable
缺点:
- 需要额外编写转换器
- 如果项目已大量使用 Newtonsoft.Json,迁移成本高
方案4:创建专门的数据传输对象(DTO)
不直接序列化异常对象,而是转换为 DTO。
public class ExceptionDto
{
public string Message { get; set; }
public string StackTrace { get; set; }
public string HelpLink { get; set; }
public ExceptionDto InnerException { get; set; }
public static ExceptionDto FromException(Exception ex)
{
if (ex == null) return null;
return new ExceptionDto
{
Message = ex.Message,
StackTrace = ex.StackTrace,
HelpLink = ex.HelpLink,
InnerException = FromException(ex.InnerException)
};
}
}
// 使用
var dto = ExceptionDto.FromException(custom);
var str = JsonConvert.SerializeObject(dto);
优点:
- 完全控制序列化的内容
- 避免序列化敏感信息
- 跨平台兼容性好
缺点:
- 无法完整还原 Exception 对象
- 需要维护额外的 DTO 类
五、最佳实践建议
5.1 开发自定义异常时
-
始终标记
[Serializable]特性:[Serializable] public class MyException : Exception { // ... } -
如果有自定义属性,实现完整的序列化支持:
[Serializable] public class MyException : Exception { public int ErrorCode { get; set; } public MyException() { } public MyException(string message) : base(message) { } public MyException(string message, Exception inner) : base(message, inner) { } // 序列化构造函数 protected MyException(SerializationInfo info, StreamingContext context) : base(info, context) { ErrorCode = info.GetInt32(nameof(ErrorCode)); } // 序列化方法 public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue(nameof(ErrorCode), ErrorCode); } }
5.2 序列化异常时
- 日志记录场景:推荐使用 DTO 模式,只序列化需要的信息
- 跨进程传输:推荐
[Serializable]+ 标准序列化 - HTTP API 响应:推荐自定义错误响应模型,不要直接暴露异常对象
5.3 .NET 版本考虑
- .NET Framework:必须使用
[Serializable]和ISerializable - .NET Core/5/6/7/8:
[Serializable]仍然推荐用于异常类(主要为了兼容性)- 二进制序列化(BinaryFormatter)已标记为过时
- JSON 序列化是主流,但建议仍保留
[Serializable]标记
六、总结
核心要点
-
[Serializable]特性不仅仅是标记:- 它会改变 Newtonsoft.Json 对对象的序列化行为
- 对于实现了
ISerializable的类(如 Exception),它会触发特殊的序列化路径
-
Exception 类的特殊性:
- 内部使用
HelpURL字段名进行序列化(不是公共属性名HelpLink) - 实现了
ISerializable,有自己的序列化逻辑 - 子类如果不标记
[Serializable],会导致序列化格式不匹配
- 内部使用
-
错误信息”Member ‘HelpURL’ was not found”的真相:
- 不是 Exception 有 HelpURL 属性
- 而是 Exception 的
ISerializable实现期望反序列化数据中有 “HelpURL” 键 - 当使用非 ISerializable 方式序列化时,生成的是 “HelpLink” 键
- 导致反序列化时找不到期望的 “HelpURL” 键
推荐方案
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 开发新的自定义异常 | 方案1:添加 [Serializable] |
最符合 .NET 规范,兼容性最好 |
| 无法修改异常类 | 方案2:配置序列化设置 | 不需要修改代码 |
| 新项目(.NET 5+) | 方案3:System.Text.Json | 更现代,性能更好 |
| 日志/API 响应 | 方案4:DTO 模式 | 安全性好,控制粒度细 |
七、参考资源
官方文档
- Microsoft Docs: Exception Class – Exception 类的 API 文档
- Microsoft Docs: ISerializable Interface – ISerializable 接口文档
- Microsoft Docs: Best Practices for Exceptions – 异常处理最佳实践
- Microsoft Docs: SerializationInfo Class – SerializationInfo 类文档
.NET 源码(? 强烈推荐阅读)
-
Exception.cs 完整源码:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Exception.cs
-
.NET Runtime GitHub 仓库:https://github.com/dotnet/runtime – .NET 核心运行时源码
-
.NET 源码浏览器:https://source.dot.net/ – 在线浏览 .NET 源码
第三方库文档
- Newtonsoft.Json 官方文档 – Json.NET 完整文档
- Newtonsoft.Json Serialization Guide – 序列化指南
- Newtonsoft.Json: Serializing ISerializable Objects – ISerializable 对象的序列化
相关技术文章
- BinaryFormatter Serialization is Obsolete – 为什么 BinaryFormatter 被废弃
- System.Text.Json vs Newtonsoft.Json – 两种 JSON 库的对比
这个问题其实是一个比较极端的场景:反序列化生成Exception对象。这个在实际应用场景中碰见的真的非常非常少,我能碰见这个问题,是因为当前项目框架的设计,业务场景出现异常后,会先将异常信息包裹为一个自定义异常对象,然后序列化为字符串,再将其作为委托参数丢到一个后台工作队列里面,在后台工作队列里面进行二次处理,这里操作逻辑就涉及到了反序列化Exception对象。
如果不是这次碰见,我还真的没有注意到这里的细节。
文章摘自:https://www.cnblogs.com/diamondhusky/p/19981804
