You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

.NET Core 3/5客户端库中如何优雅实现Exception的序列化与反序列化?

我完全懂你这种困扰——想把自定义异常直接通过API传递给客户端,这种场景太常见了,但官方文档确实没给出清晰的开箱即用方案。我之前在.NET Core 3和5里也遇到过类似问题,试过几种可行的优雅实现方式,分享给你:

核心问题先理清楚

Exception类从设计之初就不是为跨网络序列化准备的:它自带很多循环引用(比如InnerExceptionTargetSite.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.csConfigureServices里添加转换器:

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

火山引擎 最新活动