Skip to content
On this page

Messages and Serialization

The ultimate goal of Wolverine is to allow developers to route messages representing some work to do within the system to the proper handler that can handle that message. Here's some facts about messages in Wolverine:

  • By role, you can think of messages as either a command you want to execute or as an event raised somewhere in your system that you want to be handled by separate code or in a separate thread
  • Messages in Wolverine must be public types
  • Unlike other .NET messaging or command handling frameworks, there's no requirement for Wolverine messages to be an interface or require any mandatory interface or framework base classes
  • Have a string identity for the message type that Wolverine will use as an identification when storing messages in either durable message storage or within external transports

Message Type Name or Alias

Let's say that you have a basic message structure like this:

cs
public class PersonBorn
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // This is obviously a contrived example
    // so just let this go for now;)
    public int Day { get; set; }
    public int Month { get; set; }
    public int Year { get; set; }
}

snippet source | anchor

By default, Wolverine will identify this type by just using the .NET full name like so:

cs
[Fact]
public void message_alias_is_fullname_by_default()
{
    new Envelope(new PersonBorn())
        .MessageType.ShouldBe(typeof(PersonBorn).FullName);
}

snippet source | anchor

However, if you want to explicitly control the message type because you aren't sharing the DTO types or for some other reason (readability? diagnostics?), you can override the message type alias with an attribute:

cs
[MessageIdentity("person-born")]
public class PersonBorn
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Day { get; set; }
    public int Month { get; set; }
    public int Year { get; set; }
}

snippet source | anchor

Which now gives you different behavior:

cs
[Fact]
public void message_alias_is_fullname_by_default()
{
    new Envelope(new PersonBorn())
        .MessageType.ShouldBe("person-born");
}

snippet source | anchor

Message Discovery

TIP

Wolverine does not yet support the Async API standard, but the message discovery described in this section is also partially meant to enable that support later.

Strictly for diagnostic purposes in Wolverine (like the message routing preview report in dotnet run -- describe), you can mark your message types to help Wolverine "discover" outgoing message types that will be published by the application by either implementing one of these marker interfaces (all in the main Wolverine namespace):

cs
public record CreateIssue(string Name) : IMessage;
public record DeleteIssue(Guid Id) : ICommand;
public record IssueCreated(Guid Id, string Name) : IEvent;

snippet source | anchor

TIP

The marker types shown above may be helpful in transitioning an existing codebase from NServiceBus to Wolverine.

You can optionally use an attribute to mark a type as a message:

cs
[WolverineMessage]
public record CloseIssue(Guid Id);

snippet source | anchor

Or lastly, make up your own criteria to find and mark message types within your system as shown below:

cs
opts.Discovery.CustomizeHandlerDiscovery(types => types.Includes.Implements<IDiagnosticsMessageHandler>());

snippet source | anchor

Note that only types that are in assemblies either marked with [assembly: WolverineModule] or the main application assembly or an explicitly registered assembly will be discovered. See Handler Discovery for more information about the assembly scanning.

Versioning

By default, Wolverine will just assume that any message is "V1" unless marked otherwise. Going back to the original PersonBorn message class in previous sections, let's say that you create a new version of that message that is no longer structurally equivalent to the original message:

cs
[MessageIdentity("person-born", Version = 2)]
public class PersonBornV2
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
}

snippet source | anchor

The [Version("V2")] attribute usage tells Wolverine that this class is "V2" for the message-type = "person-born."

Wolverine will now accept or publish this message using the built in Json serialization with the content type of application/vnd.person-born.v2+json. Any custom serializers should follow some kind of naming convention for content types that identify versioned representations.

Serialization

Wolverine needs to be able to serialize and deserialize your message objects when sending messages with external transports like Rabbit MQ or when using the inbox/outbox message storage. To that end, the default serialization is performed with Newtonsoft.Json because of its ubiquity and "battle testedness," but you may also opt into using System.Text.Json.

When using Newtonsoft.Json, the default configuration is:

cs
return new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Auto,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects
};

snippet source | anchor

To customize the Newtonsoft.Json serialization, use this option:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.UseNewtonsoftForSerialization(settings =>
        {
            settings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
        });
    }).StartAsync();

snippet source | anchor

And to instead opt into using System.Text.Json -- which can give you better performance but with increased risk of serialization failures -- use this syntax where opts is a WolverineOptions object:

cs
opts.UseSystemTextJsonForSerialization(stj =>
{
    stj.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode;
});

snippet source | anchor

Versioned Message Forwarding

If you make breaking changes to an incoming message in a later version, you can simply handle both versions of that message separately:

cs
public class PersonCreatedHandler
{
    public static void Handle(PersonBorn person)
    {
        // do something w/ the message
    }

    public static void Handle(PersonBornV2 person)
    {
        // do something w/ the message
    }
}

snippet source | anchor

Or you could use a custom IMessageDeserializer to read incoming messages from V1 into the new V2 message type, or you can take advantage of message forwarding so you only need to handle one message type using the IForwardsTo<T> interface as shown below:

cs
public class PersonBorn : IForwardsTo<PersonBornV2>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Day { get; set; }
    public int Month { get; set; }
    public int Year { get; set; }

    public PersonBornV2 Transform()
    {
        return new PersonBornV2
        {
            FirstName = FirstName,
            LastName = LastName,
            Birthday = new DateTime(Year, Month, Day)
        };
    }
}

snippet source | anchor

Which forwards to the current message type:

cs
[MessageIdentity("person-born", Version = 2)]
public class PersonBornV2
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
}

snippet source | anchor

Using this strategy, other systems could still send your system the original application/vnd.person-born.v1+json formatted message, and on the receiving end, Wolverine would know to deserialize the Json data into the PersonBorn object, then call its Transform() method to build out the PersonBornV2 type that matches up with your message handler.

MemoryPack Serialization

Wolverine supports the high performance MemoryPack serializer through the WolverineFx.MemoryPack Nuget package. To enable MemoryPack serialization through the entire application, use:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Make MemoryPack the default serializer throughout this application
        opts.UseMemoryPackSerialization();
    }).StartAsync();

snippet source | anchor

Likewise, you can use MemoryPack on selected endpoints like this:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Use MemoryPack on a local queue
        opts.LocalQueue("one").UseMemoryPackSerialization();

        // Use MemoryPack on a listening endpoint
        opts.ListenAtPort(2223).UseMemoryPackSerialization();

        // Use MemoryPack on one subscriber
        opts.PublishAllMessages().ToPort(2222).UseMemoryPackSerialization();
    }).StartAsync();

snippet source | anchor

Released under the MIT License.