File: Serialization\TestObjectBaseConverter.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.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Serialization;

/// <summary>
/// Converter factory for <see cref="TestObject"/>-derived types that don't have their own
/// dedicated converters (e.g. not TestCase or TestResult). Serializes only the property bag
/// as "Properties", matching the DataContract serialization behavior.
/// </summary>
internal class TestObjectBaseConverterFactory : JsonConverterFactory
{
    // Singleton converter handles all TestObject-derived types via the base class,
    // avoiding MakeGenericType + Activator.CreateInstance which fail under NativeAOT.
    private static readonly TestObjectBaseConverter Converter = new();

    public override bool CanConvert(Type typeToConvert)
    {
        // Only handle the abstract TestObject base type itself. TestCase and TestResult
        // have their own dedicated converters. Other derived types are not expected on
        // the wire protocol.
        return typeToConvert == typeof(TestObject);
    }

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        return Converter;
    }
}

internal class TestObjectBaseConverter : JsonConverter<TestObject>
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert == typeof(TestObject);
    }

    public override TestObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // Always instantiate a TestCase as the carrier for the property bag.
        // TestObject is abstract, and the only concrete subclass that flows through
        // the wire protocol (other than TestCase/TestResult, which have their own
        // converters) is TestObject-as-generic-bag. Using TestCase preserves the
        // property key-value pairs for the consumer. We intentionally avoid
        // Activator.CreateInstance(typeToConvert) because it requires reflection
        // metadata that NativeAOT trims.
        var testObject = new TestCase();

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

        if (!data.TryGetProperty("Properties", out var properties) || properties.GetArrayLength() == 0)
        {
            return testObject;
        }

        foreach (var prop in properties.EnumerateArray())
        {
            if (!prop.TryGetProperty("Key", out var keyElement))
                continue;

            var testProperty = StjSafe.Deserialize<TestProperty>(keyElement.GetRawText(), options);
            if (testProperty is null)
                continue;

            if (!prop.TryGetProperty("Value", out var valueElement))
                continue;

            object? propertyData = null;
            if (valueElement.ValueKind != JsonValueKind.Null)
            {
                if (valueElement.ValueKind == JsonValueKind.String)
                {
                    propertyData = valueElement.GetString();
                }
                else
                {
                    propertyData = valueElement.GetRawText().Trim('"');
                }
            }

            testObject.SetPropertyValue(testProperty, propertyData, CultureInfo.InvariantCulture);
        }

        return testObject;
    }

    public override void Write(Utf8JsonWriter writer, TestObject value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        writer.WritePropertyName("Properties");
        writer.WriteStartArray();
        foreach (var property in value.GetProperties())
        {
            writer.WriteStartObject();
            writer.WritePropertyName("Key");
            StjSafe.Serialize(writer, property.Key, options);
            writer.WritePropertyName("Value");
            if (property.Value is null)
            {
                writer.WriteNullValue();
            }
            else
            {
                WritePropertyValue(writer, property.Value, options);
            }
            writer.WriteEndObject();
        }
        writer.WriteEndArray();

        writer.WriteEndObject();
    }

    /// <summary>
    /// Writes a property value without using
    /// JsonSerializer.Serialize(writer, value, value.GetType()) which requires
    /// reflection metadata that NativeAOT trims.
    /// </summary>
    internal static void WritePropertyValue(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        switch (value)
        {
            case string s: writer.WriteStringValue(s); break;
            case int i: writer.WriteNumberValue(i); break;
            case long l: writer.WriteNumberValue(l); break;
            case double d: writer.WriteNumberValue(d); break;
            case float f: writer.WriteNumberValue(f); break;
            case bool b: writer.WriteBooleanValue(b); break;
            case short s: writer.WriteNumberValue(s); break;
            case ushort us: writer.WriteNumberValue(us); break;
            case uint ui: writer.WriteNumberValue(ui); break;
            case ulong ul: writer.WriteNumberValue(ul); break;
            case byte by: writer.WriteNumberValue(by); break;
            case sbyte sb: writer.WriteNumberValue(sb); break;
            case decimal dec: writer.WriteNumberValue(dec); break;
            case char c: writer.WriteStringValue(c.ToString()); break;
            case DateTimeOffset dto: writer.WriteStringValue(dto); break;
            case DateTime dt: writer.WriteStringValue(dt); break;
            case Guid g: writer.WriteStringValue(g); break;
            case Uri u: writer.WriteStringValue(u.OriginalString); break;
            case JsonElement je: je.WriteTo(writer); break;
            case TimeSpan ts: writer.WriteStringValue(ts.ToString()); break;
            case Enum e:
                // Write enums as their underlying numeric value.
                writer.WriteNumberValue(Convert.ToInt64(e, CultureInfo.InvariantCulture));
                break;
            case string[] sa:
                writer.WriteStartArray();
                foreach (var item in sa) writer.WriteStringValue(item);
                writer.WriteEndArray();
                break;
            case KeyValuePair<string, string>[] kvps:
                writer.WriteStartArray();
                foreach (var kvp in kvps)
                {
                    writer.WriteStartObject();
                    writer.WriteString("Key", kvp.Key);
                    writer.WriteString("Value", kvp.Value);
                    writer.WriteEndObject();
                }
                writer.WriteEndArray();
                break;
            case IDictionary dict:
                writer.WriteStartObject();
                foreach (DictionaryEntry entry in dict)
                {
                    writer.WritePropertyName(Convert.ToString(entry.Key, CultureInfo.InvariantCulture)!);
                    if (entry.Value is null) writer.WriteNullValue();
                    else WritePropertyValue(writer, entry.Value, options);
                }
                writer.WriteEndObject();
                break;
            case IEnumerable enumerable:
                writer.WriteStartArray();
                foreach (var item in enumerable)
                {
                    if (item is null) writer.WriteNullValue();
                    else WritePropertyValue(writer, item, options);
                }
                writer.WriteEndArray();
                break;
            default:
                // Last resort for types not handled above. Under NativeAOT this may
                // fail for types not in the source-gen context, but all known property
                // value types used in the wire protocol are handled explicitly.
                var element = StjSafe.SerializeToElement(value, value.GetType(), options);
                element.WriteTo(writer);
                break;
        }
    }
}

#endif