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 I do, however, understand if this represents a real barrier.