Search code examples
c#serializationmasstransit

Configure MassTransit to deserialize polymorphic properties


So, we are sending a message composed of a complex domain type. Our consumers are not triggering because MassTransit cannot deserialize the message and delegate to the consumer.

This scenario may be demonstrated via

// message interface
public interface ITestMessage { TestBaseClass Data { get; set; } };

// message implementation
public class TestMessage : ITestMessage
{
    public TestBaseClass Data { get; set; }
}

// abstract child
public abstract class TestBaseClass { }

// a concrete implementation of abstract child, cannot be deserialized 
// by default serializer configuration
public class TestConcreteClass : TestBaseClass { }

// simple consumer, uses a reset-event to synchronize with calling
// test method
public class TestConsumer : IConsumer<ITestMessage>
{
    private readonly Action action = null;
    public TestConsumer(Action action) { this.action = action; }
    public Task Consume(ConsumeContext<ITestMessage> context)
    {
        action();
        return context.CompleteTask;
    }
}

[TestMethod]
public void Publish_WhenPolymorphicMessage_ConsumesMessage()
{
    ManualResetEvent isConsumed = new ManualResetEvent(false);
    IBusControl bus = Bus.Factory.CreateUsingInMemory(c =>
    {
        InMemoryTransportCache inMemoryTransportCache = 
            new InMemoryTransportCache(Environment.ProcessorCount);
        c.SetTransportProvider(inMemoryTransportCache);
        c.ReceiveEndpoint(
            "", 
            e => e.Consumer<TestConsumer>(
                () => new TestConsumer(() => isConsumed.Set())));
    });
    bus.Start();

    ITestMessage message = new TestMessage 
    {
        // comment out assignment below, et voila, we pass :S
        Data = new TestConcreteClass { },
    };

    // attempt to publish message and wait for consumer
    bus.Publish<ITestMessage>(message);

    // simple timeout fails
    Assert.IsTrue(isConsumed.WaitOne(TimeSpan.FromSeconds(5)));
}

Of course, we may successfully demonstrate that a message with polymorphic child can be serialized

[TestMethod]
public void Serialize_WithPolymorphicChild_DeserializesCorrectly()
{
    ITestMessage message = new TestMessage { Data = new TestConcreteClass { }, };
    JsonSerializerSettings settings = 
        new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, };
    string messageString = JsonConvert.SerializeObject(message, settings);
    ITestMessage messageDeserialized = (ITestMessage)(JsonConvert.DeserializeObject(
        messageString, 
        settings));

    Assert.IsNotNull(messageDeserialized);
    Assert.IsNotNull(messageDeserialized.Data);
    Assert.IsInstanceOfType(messageDeserialized.Data, typeof(TestConcreteClass));
    Assert.AreNotSame(message.Data, messageDeserialized.Data);
}

I have attempted various configurations, to no avail.

IBusControl bus = Bus.Factory.CreateUsingInMemory(c =>
{
    InMemoryTransportCache inMemoryTransportCache = 
        new InMemoryTransportCache(Environment.ProcessorCount);
    c.SetTransportProvider(inMemoryTransportCache);

    // attempt to set and configure json serializer; zero effect
    c.ConfigureJsonDeserializer(
        s => 
        {
            s.TypeNameHandling = TypeNameHandling.All; 
            return s;
        });
    c.ConfigureJsonSerializer(
        s => 
        {
            s.TypeNameHandling = TypeNameHandling.All; 
            return s; 
        });
    c.UseJsonSerializer();

    c.ReceiveEndpoint(
        "", 
        e => e.Consumer<TestConsumer>(
            () => new TestConsumer(() => isConsumed.Set())));
});

I am seeking any successful solution, eg adornment of message/domain classes with KnownType attributes, or bus configuration. Anything that leads to a successful pass of test method above.


Solution

  • Changing your ITestMessage interface declaration to:

    // message interface
    public interface ITestMessage
    {
        [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
        TestBaseClass Data { get; set; }
    };
    

    Resolved the issue for me.