Support for immutable messages

I’d like to suggest that NServiceBus supports immutable messages. I’ve been asking around a bit, and it looks like something that may be enabled by certain community packages, but right now I’m working with a customer who’s new to NServiceBus, and while they like it a lot, I don’t think it’s a good idea to lead them off the beaten path yet.

Besides, this is a feature or capability that would, I believe, be generally beneficial, although I by no means suggest that this is made mandatory.

Let me explain first what I’d like to have, and afterwards, why it’s a good idea.

Context

Currently, messages are defined as dumb DTOs like this:

public class ReserveTable : ICommand, IMessage
{
    public Guid Id { get; set; }
    public DateTime Time { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public int Quantity { get; set; }
}

(Please accept my apologies if the code doesn’t compile; I’m writing this without IDE assistance.)

It’s easy to define message types like that, but writing code that consumes such a type is bothersome because of non-existing invariants.

Such a design causes several problems. I’ll outline some later in this post.

Feature description

I’d like to be able to instead define messages as immutable records, like this:

public class ReserveTable : ICommand, IMessage
{
    public ReserveTable(Guid id, DateTime time, string name, string email, int quantity)
    {
        // Initialise all properties with constructor arguments here...
    }

    public Guid Id { get; }
    public DateTime Time { get; }
    public string Name { get; }
    public string Email { get; }
    public int Quantity { get; }

    public override Equals // ...
    public override GetHashCode // ...
}

Yes, that’s clearly more work when defining the message types, but it makes consuming messages, including unit testing, much simpler. It’s a trade-off I’d make every time, if enabled.

Problems with mutable messages

A mutable message is conceptually wrong. This is particularly clear for events. With mutable messages (the current design) a message handler could easily change one or more properties on the message it handles. That’s odd when the message represents an event that has already happened.

Additionally, messages carry no invariants. Properties can be null, even when conceptually required. This leads to much defensive coding that could otherwise be avoided.

Problems related to unit testing

A common pattern when unit testing is the Test Data Builder design pattern. This enables you to write tests that only explicitly state those things that are important for a test case.

Imagine writing a unit test for a mutable ReserveTable command. In this test case, Name and Email aren’t important. They could be anything, because they don’t impact the behaviour of the program.

Test Data Builders

One can use the Test Data Builder pattern to address such a concern:

    var message = new ReserveTableBuilder()
        .WithDate(/* ... */)
        .WithQuantity(4)
        .Build();
    // The rest of the test goes here...

While elegant, this requires you to define a test-specific ReserveTableBuilder class. Not only that, but if you need it in more than one unit test project (which is likely, as message types are what glues message-based applications together), you’ll either have to duplicate those builder classes, or put them in a shared ‘unit test util’ library.

Equality comparison

Another unit testing problem is that you’d often need to compare an expected message to an actual message. This is possible (particularly with xUnit.net) using custom comparers, like this:

public class ReserveTableComparer : IEqualityComparer<ReserveTable>
{
    public bool Equals(ReserveTable x, ReserveTable y)
    {
        return Equals(x.Id, y.Id)
            && Equals(x.Time, y.Time)
            && Equals(x.Name, y.Name)
            && Equals(x.Email, y.Email)
            && Equals(x.Quantity, y.Quantity);
    }

    public int GetHashCode(CustomerDeleted obj) // ...
}

Again, you’ll have to add such a comparer to your unit test code base, and again may have to deal with duplcation.

It’d be nice if one could just override Equals for the message itself, but unfortunately, structural equality is a really bad idea for mutable objects.

How immutable messages are better

Immutable messages address all of those concerns. First of all, messages, particularly events, ought to be conceptually immutable.

Also, you don’t need to add and maintain separate test-specific Test Data Builder classes. Just add WithXyz methods to the immutable record type itself:

public class ReserveTable : ICommand, IMessage
{
    public ReserveTable(Guid id, DateTime time, string name, string email, int quantity) // ...

    // ...
    public int Quantity { get; }

    public ReserveTable WithQuantity(int newQuantity)
    {
        return new ReserveTable(Id, Time, Name, Email, newQuantity);
    }

    // ...
}

All you have to do in your test code is to define good default test values. All the benefits you get from the Test Data Builder pattern now comes with the type itself (and they’re also available for the production code).

Additionally, immutable records can safely have structural equality, so one can safely override Equals on those classes. No customer comparers are required, making testing even easier.

Summary

Immutable message types offers plenty of advantages:

  • Conceptually correct for messaging
  • Better invariants
  • No need for Test Data Builders
  • No need for custom equality comparers

The main disadvantages, as I see them, are:

  • More typing is required to define the message types
  • Serialisation and deserialisation may require more custom coding, since this isn’t a common idiom on .NET

I’m personally not concerned about having to do a bit more up-front typing. This sort of design ultimately requires less typing, and the overall code becomes easier to read and maintain.

Or, if one is much concerned about typing, one can use a DSL to define immutable message types:

type ReserveTable = { Id : Guid; Time : DateTime; Name : string; Email : string; Quantity : int }

This type declaration generates IL code that corresponds to the immutable classes I’ve described above.

The serialisation issue is an internal NServiceBus issue, so doesn’t concern me as a user :grinning: I do, however, understand if this represents a real barrier.

@ploeh That already works, just use a serializer that supports this like Newtonsoft Json.

public class MyMessage : IMessage
{
    public MyMessage(int value)
    {
        Value = value;
    }

    public int Value { get; private set; }

    public override string ToString()
    {
        return Value.ToString(CultureInfo.InvariantCulture);
    }
}

This serializer package is fully supported by Particular Software although Newtonsoft Json isn’t maintained by us.

Likely other serializers support deserialization to private properties too but we do not maintain a list.

1 Like

Oh, that’s good to hear :grinning:

I had the (wrong) impression that this wasn’t supported, but now I see how it works. Obviously, Particular Software can’t support JSON.NET itself; I think that was where I got it wrong.

I’ll see if I can try it out tomorrow when I’m visiting my client.

Thank you

@ploeh,

Here are a couple of samples on how to achieve immutable messages: GitHub - mauroservienti/immutable-message-samples

.m

Hi,

As Ramon said, what you’re after is possible if you’re using Json Serialization.
I agree 100%, messages are contracts and immutable and should be represented as such in your code as well.

Cheers,
Indu Alagarsamy
Particular Software

Thanks, everyone. The preliminary experiments we did Friday convinced us to move further in this direction. No problems encountered yet :grinning:

Just one thing to very cognizant of here - the types are merely a specification for a message, they are not the message itself. The message is what’s on the wire, an agreement of a contract. If you want to have a formal definition of a message, that’s a schema.

I tend to avoid any sort of behavior on my message types, to avoid confusion about what those types represent. They’re a blueprint, a helper for serialization purposes. The message is not a type, it’s a manifestation of a schema.

If C# ever gets record types, it would be a good fit, but until it does, I’ve found trying to twist C# and other frameworks into immutable types isn’t really worth the effort for the gain.

Instead, I tell our clients to treat the message types no different than you would for View Models or API models in an ASP.NET Core application. Dumb DTOs.

Jimmy, thank you for replying. To be clear, treating message types as DTOs is exactly what I’m after here. I consider immutable records to have less behaviour than mutable objects.

Where we seem to disagree is whether it’s worth pursuing immutable records in C#. I think that it is, for the reasons outlined above.

Sorry for hijacking this 2 month old topic…

I have a couple of questions:

  1. Does it make sense to have a F# project solely to declare immutable types and reference them on a C# project?

  2. How do you handle collections immutability? That’s how I’m thinking of doing it:

public class ReserveTable : ICommand, IMessage
{
    public ReserveTable(Guid id, DateTime time, string name, string email, int quantity, List<Item> items)
    {
        // Initialise all properties with constructor arguments here...

        Items = items.AsReadOnly();
    }

    public Guid Id { get; }
    public DateTime Time { get; }
    public string Name { get; }
    public string Email { get; }
    public int Quantity { get; }
    public ReadOnlyCollection<Item> Items { get; }

    public override Equals // ...
    public override GetHashCode // ...
}

I know it’s not related to NServiceBus at all. Anyway, I really appreciate any feedback.

Thank you.

It might. What tends to happen, though, is that once you have that library there, you also start to add small pieces of behaviour into it, if you can figure out how to write F# at all. I don’t consider that a bad thing by any means :slight_smile:

One thing about F# records that can be a bit annoying from C#, however, is the lack of copy-and-update support. In F#, it’s part of the language syntax, which enables you to write

let p1 = { p with Age = 42 }

In C#, instead, you’d have to write

var p1 = new Person(p.FirstName, p.LastName, 42);

every time you have to ‘modify’ a part of a record. You can make this less cumbersome by adding extension methods, but you’d have to write those extension methods yourself:

public static Person WithAge(this Person p, int newAge)
{
    return new Person(p.FirstName, p.LastName, newAge);
}

This would enable you to write

var p1 = p.WithAge(42);

It’d be nice if the F# compiler would add such methods to the IL when it compiles a record type…

Use IEnumerable<T> or IReadOnlyCollection<T>.

1 Like

Out of curiosity, why do you need this for messages? I’m clearly in @jbogard his camp to just treat messages as DTO’s. Really interesting in the reasons for needing immutable types besides the reasons that @ploeh indicated.

If you really want that behavior I probably would override the deserializer or add a pipeline extension that does List<T>.AsReadonly() on each collection

My additional perspective on immutable messages:

If there is a need to have immutable collections during processing then only at that moment I would just clone the data into an immutable structure. Very often such a structure is already different from the message schema.

In general, it is a rule to not modify incoming data that is passed by reference if you do not own that data to prevent side effects.

I personally think it’s unneeded as it adds a lot of complexity and code just for the situation that recipients might change DTO data either intentionally or by accident. After all, the actual physical incoming message from the wire will not change.

– Ramon

@jbogard or @ploeh,
As now C#9 has first native implementation of Record types, Is it getting better?

That’ll depend on your serializer of choice.

@ploeh of course there is no reason why you can’t just use NServiceBus from F# altogether.

I have implemented multiple systems using NServiceBus with 99% F# code. With the exception of simple saga implementations everything can be done in F#.

As for immutable messages, you can use F# records (with CLIMutable for serialization) and then consume them in F# handlers, where they are truly immutable. This doesn’t protect NSB Infrastructure code from mutation, but all your own code will be safe.

It would be nice for NSB to support message immutability in the pipeline, of course.