.NET Core 3/5客户端库中如何优雅实现Exception的序列化与反序列化?
我完全懂你这种困扰——想把自定义异常直接通过API传递给客户端,这种场景太常见了,但官方文档确实没给出清晰的开箱即用方案。我之前在.NET Core 3和5里也遇到过类似问题,试过几种可行的优雅实现方式,分享给你:
核心问题先理清楚
Exception类从设计之初就不是为跨网络序列化准备的:它自带很多循环引用(比如InnerException和TargetSite.DeclaringType的嵌套引用),还有像System.Type这种本身无法被默认序列化器处理的成员,这就是你在两个版本里遇到不同报错的根源。
方案一:自定义System.Text.Json转换器(推荐,无额外依赖)
因为.NET Core 3+默认使用System.Text.Json,自定义转换器可以完全掌控序列化逻辑,同时解决3.x的循环引用/深度问题和5.x的Type序列化问题。
1. 编写Exception转换器
这个转换器会手动序列化我们需要的异常属性,跳过那些导致问题的字段,同时支持自定义异常的额外属性:
using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; public class ExceptionConverter : JsonConverter<Exception> { public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); var root = doc.RootElement; // 读取异常类型信息 if (!root.TryGetProperty("Type", out var typeElement)) throw new JsonException("Missing 'Type' property in exception JSON"); var exceptionType = Type.GetType(typeElement.GetString()); if (exceptionType == null) throw new JsonException($"Could not load type {typeElement.GetString()}"); // 创建异常实例(使用带Message的构造函数) var exception = (Exception)Activator.CreateInstance(exceptionType, root.GetProperty("Message").GetString()); // 赋值私有字段:StackTrace if (root.TryGetProperty("StackTrace", out var stackTraceElement)) exception.SetFieldValue("_stackTraceString", stackTraceElement.GetString()); // 处理InnerException if (root.TryGetProperty("InnerException", out var innerExceptionElement) && !innerExceptionElement.ValueKind.Equals(JsonValueKind.Null)) { var innerException = JsonSerializer.Deserialize<Exception>(innerExceptionElement.GetRawText(), options); exception.SetFieldValue("_innerException", innerException); } // 序列化自定义异常的额外属性 foreach (var prop in exceptionType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => !new[] { nameof(Exception.InnerException), nameof(Exception.Message), nameof(Exception.StackTrace), nameof(Exception.TargetSite) }.Contains(p.Name))) { if (root.TryGetProperty(prop.Name, out var propElement) && !propElement.ValueKind.Equals(JsonValueKind.Null)) { var propValue = JsonSerializer.Deserialize(propElement.GetRawText(), prop.PropertyType, options); prop.SetValue(exception, propValue); } } return exception; } public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) { writer.WriteStartObject(); // 写入异常类型全名称(用于反序列化) writer.WriteString("Type", value.GetType().AssemblyQualifiedName); writer.WriteString("Message", value.Message); writer.WriteString("StackTrace", value.StackTrace); // 递归序列化InnerException if (value.InnerException != null) { writer.WritePropertyName("InnerException"); JsonSerializer.Serialize(writer, value.InnerException, options); } // 写入自定义异常的额外属性 foreach (var prop in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => !new[] { nameof(Exception.InnerException), nameof(Exception.Message), nameof(Exception.StackTrace), nameof(Exception.TargetSite) }.Contains(p.Name))) { var propValue = prop.GetValue(value); if (propValue != null) { writer.WritePropertyName(prop.Name); JsonSerializer.Serialize(writer, propValue, options); } } writer.WriteEndObject(); } } // 扩展方法:设置Exception的私有字段 public static class ExceptionExtensions { public static void SetFieldValue(this Exception exception, string fieldName, object value) { var field = typeof(Exception).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(exception, value); } }
2. 注册转换器到MVC服务
在Startup.cs的ConfigureServices里添加转换器:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new ExceptionConverter()); }); }
3. 客户端反序列化
客户端只需使用同样的转换器,就能把JSON还原为自定义异常:
var response = await httpClient.GetAsync("/api/test"); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { var json = await response.Content.ReadAsStringAsync(); var exception = JsonSerializer.Deserialize<FooNotFoundException>(json, new JsonSerializerOptions { Converters = { new ExceptionConverter() } }); // 在这里处理FooNotFoundException }
方案二:切换到Newtonsoft.Json(快速实现)
如果不想写自定义转换器,Json.NET(Newtonsoft.Json)对Exception序列化的支持更成熟,只需简单配置就能解决问题:
1. 安装NuGet包
Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson
2. 配置MVC服务
在Startup.cs里配置循环引用忽略、类型名称处理,并忽略序列化错误:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddNewtonsoftJson(options => { // 忽略循环引用 options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // 写入类型信息,方便客户端反序列化 options.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects; // 忽略无法序列化的属性(比如TargetSite.DeclaringType) options.SerializerSettings.Error = (sender, args) => args.ErrorContext.Handled = true; }); }
3. 客户端反序列化
客户端用Json.NET反序列化,注意开启TypeNameHandling,同时为了安全,最好限定允许的类型:
using Newtonsoft.Json; var response = await httpClient.GetAsync("/api/test"); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { var json = await response.Content.ReadAsStringAsync(); var exception = JsonConvert.DeserializeObject<FooNotFoundException>(json, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Objects, // 自定义绑定器,只允许反序列化我们的异常类型,避免安全风险 SerializationBinder = new SafeSerializationBinder() }); // 处理异常 } // 安全序列化绑定器示例 public class SafeSerializationBinder : ISerializationBinder { public Type BindToType(string assemblyName, string typeName) { // 只允许反序列化我们自定义的异常类型 var allowedTypes = new[] { typeof(FooNotFoundException), typeof(Exception) }; return allowedTypes.FirstOrDefault(t => t.FullName == typeName); } public void BindToName(Type serializedType, out string assemblyName, out string typeName) { assemblyName = serializedType.Assembly.FullName; typeName = serializedType.FullName; } }
注意事项
- 确保客户端程序集中存在对应的自定义异常类型,否则反序列化会失败。
- 使用
TypeNameHandling时一定要注意安全,必须用SerializationBinder限定允许的类型,防止反序列化恶意代码。 - 自定义转换器可以根据需求调整序列化的字段,只保留必要信息,减少传输的数据量。
内容的提问来源于stack exchange,提问作者Matt Hintzke




