File: Serialization\ExceptionConverter.cs
Web Access
Project: src\vstest\src\Microsoft.TestPlatform.CommunicationUtilities\Microsoft.TestPlatform.CommunicationUtilities.csproj (Microsoft.TestPlatform.CommunicationUtilities)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETCOREAPP

using System;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Serialization;

/// <summary>
/// JSON converter for <see cref="Exception"/> that handles the read-only properties
/// (<c>Message</c>, <c>StackTrace</c>) which STJ's source-generated metadata cannot populate
/// via the parameterless constructor.
/// <para>
/// On deserialization, produces a <see cref="RemoteException"/> that preserves the original
/// type name, message, stack trace, and inner exception. The original exception type is erased
/// (all exceptions materialize as <see cref="RemoteException"/>) but <c>ToString()</c> renders
/// the full original information including type name and stack trace.
/// </para>
/// </summary>
internal class ExceptionConverter : JsonConverter<Exception>
{
    /// <inheritdoc/>
    public override Exception? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return null;
        }

        using var doc = JsonDocument.ParseValue(ref reader);
        var root = doc.RootElement;

        string? className = root.TryGetProperty("ClassName", out var clsProp) && clsProp.ValueKind == JsonValueKind.String
            ? clsProp.GetString()
            : null;

        string? message = root.TryGetProperty("Message", out var msgProp) && msgProp.ValueKind != JsonValueKind.Null
            ? msgProp.GetString()
            : null;

        string? stackTrace = root.TryGetProperty("StackTraceString", out var stProp) && stProp.ValueKind == JsonValueKind.String
            ? stProp.GetString()
            : null;

        Exception? innerException = null;
        if (root.TryGetProperty("InnerException", out var innerProp) && innerProp.ValueKind != JsonValueKind.Null)
        {
            innerException = StjSafe.Deserialize<Exception>(innerProp.GetRawText(), options);
        }

        var exception = new RemoteException(className, message, stackTrace, innerException);

        if (root.TryGetProperty("HResult", out var hresultProp) && hresultProp.ValueKind == JsonValueKind.Number)
        {
            exception.HResult = hresultProp.GetInt32();
        }

        if (root.TryGetProperty("Source", out var sourceProp) && sourceProp.ValueKind == JsonValueKind.String)
        {
            exception.Source = sourceProp.GetString();
        }

        return exception;
    }

    /// <inheritdoc/>
    public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        // For RemoteException (already deserialized once), preserve the original ClassName.
        writer.WriteString("ClassName", value is RemoteException remote
            ? remote.ClassName
            : value.GetType().FullName);
        writer.WriteString("Message", value.Message);
        writer.WriteString("StackTraceString", value is RemoteException re
            ? re.RemoteStackTrace
            : value.StackTrace);
        writer.WriteString("Source", value.Source);
        writer.WriteNumber("HResult", value.HResult);

        writer.WritePropertyName("InnerException");
        if (value.InnerException is null)
        {
            writer.WriteNullValue();
        }
        else
        {
            StjSafe.Serialize(writer, value.InnerException, options);
        }

        writer.WriteEndObject();
    }
}

/// <summary>
/// Exception that preserves diagnostic information from a remotely-serialized exception,
/// including the original type name and stack trace. Since the original exception type may
/// not be available (or may be trimmed under NativeAOT), this type acts as a carrier that
/// faithfully reproduces the original <c>ToString()</c> output.
/// </summary>
internal sealed class RemoteException : Exception
{
    /// <summary>
    /// Gets the fully qualified name of the original exception type (e.g.
    /// <c>"System.InvalidOperationException"</c>).
    /// </summary>
    public string? ClassName { get; }

    /// <summary>
    /// Gets the original stack trace string as captured from the remote process.
    /// </summary>
    public string? RemoteStackTrace { get; }

    public RemoteException(string? className, string? message, string? stackTrace, Exception? innerException)
        : base(message, innerException)
    {
        ClassName = className;
        RemoteStackTrace = stackTrace;
    }

    public override string ToString()
    {
        var sb = new StringBuilder();
        sb.Append(ClassName ?? nameof(RemoteException));
        if (!string.IsNullOrEmpty(Message))
        {
            sb.Append(": ").Append(Message);
        }

        if (InnerException is not null)
        {
            sb.Append(" ---> ").Append(InnerException).AppendLine()
              .Append("   --- End of inner exception stack trace ---");
        }

        if (!string.IsNullOrEmpty(RemoteStackTrace))
        {
            sb.AppendLine().Append(RemoteStackTrace);
        }

        return sb.ToString();
    }
}

#endif