How to safely deserialize MessagePack messages without throwing exceptions?

810 Views Asked by At

It's my DTO:

public class InvocationMessage
{
    public string MethodName { get; set; }
    public object?[]? Args { get; set; }
}

And initially I serialized it using MessagePackSerializer.Serialize(Msg, ContractlessStandardResolver.Options);

Then while deserializing I wanted to convert object from Args to concrete types. It occured to be impossible without custom formatters. So I wrote one:

public InvocationMessage Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
    var result = new InvocationMessage
    {
        MethodName = reader.ReadString()
    };

    // Hardcoded for testing purpose
    var argTypes = new Type[] { typeof(SomeType) };
    if (reader.TryReadArrayHeader(out int count))
    {
        result.Args = new object?[count];

        for (int i = 0; i < argTypes.Length; i++)
        {
            result.Args[i] = MessagePackSerializer.Deserialize(argTypes[i], ref reader,
                ContractlessStandardResolver.Options.WithSecurity(MessagePackSecurity.UntrustedData));
        }
    }

    return result;
}

public void Serialize(ref MessagePackWriter writer, InvocationMessage value, MessagePackSerializerOptions options)
{
    var methodName = Encoding.UTF8.GetBytes(value.MethodName);

    writer.WriteString(methodName);

    if (value.Args is not null && value.Args.Length > 0)
    {
        writer.WriteArrayHeader(value.Args.Length);

        foreach (var arg in value.Args)
        {
            MessagePackSerializer.Serialize(ref writer, arg, ContractlessStandardResolver.Options);
        }
    }
}

Unfortunelly, the exception is always thrown when I try to deserialize e.g. something like this:

var msg = new InvocationMessage
{
    MethodName = "sometestname",
    Args = new object?[]
        {
            new 
            {
                Test = 3
            },
            new
            {
                ReceiverId = "12ds",
                Content = "s",
            },
            new 
            {
                Test2 = 5
            },
        }
};

It's because I want to deserialize some unknown type to my SomeType, or because my SomeType wants string for property ReceiverId and gets int. There are many cases where it throws.

But I need to avoid throws because it's big performance issue for my app. There is no something like TryDeserialize method, so I don't have any idea what can I do.

1

There are 1 best solutions below

3
Freeman On

I think it's better to edit your deserialization code to use a custom formatter that handles unknown types or type mismatches, so first of all define a custom formatter for your InvocationMessage

public class InvocationMessageFormatter : IMessagePackFormatter<InvocationMessage>
{
    public InvocationMessage Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
    {
        var result = new InvocationMessage
        {
            MethodName = reader.ReadString()
        };

        if (reader.TryReadArrayHeader(out int count))
        {
            result.Args = new object?[count];

            for (int i = 0; i < count; i++)
            {
                result.Args[i] = DeserializeObject(ref reader, options);
            }
        }

        return result;
    }

    public void Serialize(ref MessagePackWriter writer, InvocationMessage value, MessagePackSerializerOptions options)
    {
        writer.WriteString(value.MethodName);

        if (value.Args is not null && value.Args.Length > 0)
        {
            writer.WriteArrayHeader(value.Args.Length);

            foreach (var arg in value.Args)
            {
                SerializeObject(ref writer, arg, options);
            }
        }
    }

    private object? DeserializeObject(ref MessagePackReader reader, MessagePackSerializerOptions options)
    {
        /* you can return null or a default value for unknown types,
         or convert the object to the expected type if possible.
         I'll simply return null for unknown types */
        return null;
    }

    private void SerializeObject(ref MessagePackWriter writer, object? value, MessagePackSerializerOptions options)
    {
        //you can throw an exception or convert the object to a compatible type,for example, I'll do nothing when serializing unknown types
    }
}

now you can easily register the custom formatter in your serialization/deserialization code

var options = MessagePackSerializerOptions.Standard.WithResolver(ContractlessStandardResolver.Options);
options = options.WithFormatter<InvocationMessage>(new InvocationMessageFormatter());

//serialize
var serializedBytes = MessagePackSerializer.Serialize(msg, options);

//deserialize
var deserializedMsg = MessagePackSerializer.Deserialize<InvocationMessage>(serializedBytes, options);