Reflection for testing serialization

November 18, 2024    .Net AutomatedTesting

Reflection for testing serialization

an image of a mirror reflecting a computer screen full of .net C# code - generated by Microsoft CoPilot

I had a failure when a class couldn’t serialize to JSON for a logging call (old classes that mix data and functions, with dependency injection and the .Net 8 Newtonsoft update blew up. I wish I could refactor the properties from the function, but don’t have the time now for changes and testing).

We are using Newtonsoft.Json.

It was failing on this logging line we have:

if (_logMessage) op.Telemetry.Properties.Add("payload", message.SafeToJson());

Here’s the exception "System.IO.FileNotFoundException: 'Could not load file or assembly 'Mindbox.Data.Linq, Version=3.2.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.'"

I don’t want serialization to try to load files just to give us a property. So I added [JsonObject(MemberSerialization.OptIn)] and [JsonProperty]

A Class Example

using Newtonsoft.Json;
namespace Me;

[JsonObject(MemberSerialization.OptIn)]
public class MyMessage(ISomeService _someService) : base(_someService), IMessage{
   [JsonProperty]
   public Guid AccountId;

   public async Task<Result> Process(CancellationToken){
     // code to process
   }
}

SafeToJson

    public static string SafeToJson(this object value) => SafeToJson(value, Formatting.Indented);

    public static string SafeToJson(this object value, Formatting formatting)
    {
        try
        {
            var settings = new JsonSerializerSettings
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            };

            return JsonConvert.SerializeObject(value, formatting, settings);
        }
        catch (Exception)
        {
            // could pss in ILogger and log
            return "{}";
        }
    }

The Unit Test

namespace Me.Tests;
public class IMessageSerializationTests(ITestOutputHelper _testOutputHelper)
{

    private static object GetDefaultValue(Type type)
    {
        // Return default values for value types and null for reference types
        if (type.IsValueType) { return Activator.CreateInstance(type); }
        return null;
    }

    [Fact]
    public void CanSerializeAllIMessages()
    {
        // Load the assembly
        Assembly assembly = Assembly.GetAssembly(typeof(MyClassInTheAssembly));

        // Get all types in the assembly
        Type[] types = assembly.GetTypes();

        // Filter types based on a specific condition (e.g., all classes)
        var classes = types
            .Where(t => typeof(IMessage).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract)
            // skip no property messages
            .Where(t => t.Name != nameof(AMessageWithoutProperties))
            .ToList();
        var anyFailed = false;
        foreach (var type in classes)
        {
            // Prepare default values for the constructor parameters
            var constructor = type.GetConstructors().FirstOrDefault();
            var parameters = constructor.GetParameters();
            var arguments = parameters.Select(p => GetDefaultValue(p.ParameterType)).ToArray();

            // Thanks Microsoft CoPilot for help with this code :-)
            try
            {
                var instance = Activator.CreateInstance(type, arguments);
                var json = instance.SafeToJson();
                Assert.NotEqual("{}", json);
                _testOutputHelper.WriteLine($"serialized {type.FullName}");
            }
            catch (Exception ex)
            {
                _testOutputHelper.WriteLine($"failed {ex} {type.FullName}");
                anyFailed = true;
            }
        }

        Assert.False(anyFailed, "All IMessages can be serialized for the Telemetry and Json");
    }
}

We could leverage source generation to automate reflection-based tasks, enhancing performance and maintainability, but this unit test doesn’t have to be that fast.

Now I know the messages will serialize and I didn’t miss adding [JsonObject(MemberSerialization.OptIn)] for new classes.