Skip to main content

What's new in NServiceBus 7.4

At Particular Software, we don’t believe in packaging all the cool new features into major versions that make it hard to upgrade. We prefer to deliver improvements incrementally in minor releases that are easy upgrades, deferring breaking changes until the next major release to clean things up before the next set of incremental improvements.

That means that when it comes to new goodies, minor releases are where it’s at, and this minor release is no exception.

NServiceBus 7.4 includes some great new enhancements that make mapping sagas easier and more powerful, enable multiple different strategies for message conventions, and subdivide a message flow into multiple conversations.

Let’s take a closer look.

🔗Finding sagas

Saga mapping is a configuration activity that tells the platform how to find a particular saga instance when it is processing a message. We’ve created a new API that makes mapping sagas more straightforward and less duplicative.

This is what saga mapping used to look like:

public override void ConfigureMapping(SagaPropertyMapper<MySagaData> mapper)
{
  mapper.CreateMapping<OrderShipped>(msg => msg.CustomerId).ToSaga(saga => saga.CustomerId);
  mapper.CreateMapping<OrderBilled>(msg => msg.CustomerId).ToSaga(saga => saga.CustomerId);
}

This API works, and there are thousands of sagas out there that are defined in this way. However, the existing API isn’t very obvious, and results in a lot of duplication especially with multiple mapped messages.

For historical reasons, each message mapping has to define how it correlates to a saga. That’s the ToSaga() call, and if you leave it out the message doesn’t get mapped. The API also implies that each message can correlate to the saga with a different property, but that doesn’t work. Sagas can only have one correlation property, so we check for that and throw an exception if you try.

We introduced a new API that maps the saga correlation property first and then uses that correlation to create a mapping for each message.

public override void ConfigureMapping(SagaPropertyMapper<MySagaData> mapper)
{
  mapper.MapSaga(saga => saga.CustomerId)
    .ToMessage<OrderShipped>(msg => msg.CustomerId)
    .ToMessage<OrderBilled>(msg => msg.CustomerId);
}

This new API better represents the intent of the mapping and results in cleaner, simpler saga mapping with no unnecessary duplication.

Note: If you use SQL Persistence, that will also need to be upgraded due to how SQL Persistence analyzes the source code of the ConfigureMapping method to determine the correlation property.

🔗Saga mapping by headers

While we were working with the saga mapping API, we took the opportunity to enable saga mapping via a message header. In this example, when processing an OrderShipped message, the system will use the Shipping.CustomerId header to find a saga instance.

public override void ConfigureMapping(SagaPropertyMapper<MySagaData> mapper)
{
  mapper.MapSaga(saga => saga.CustomerId)
    .ToMessageHeader<OrderShipped>("Shipping.CustomerId")
    .ToMessage<OrderBilled>(msg => msg.CustomerId);
}

The sample also demonstrates that you can mix and match message property and message header correlation within a single saga. For more complex saga mappings, you can always create a behavior that performs your correlation logic and elevates the correlation property into a header on the message.

It’s important to note that the existing saga mapping API is not being deprecated, so if your sagas are working just fine, there’s no need to do anything. But we think you’ll like the more concise API for all of your new sagas going forward.

🔗Multiple message conventions

In NServiceBus version 7.4, we’ve introduced a way to support multiple rules for message conventions.1

NServiceBus comes with easy-to-use message conventions based on marker interfaces. Commands implement the ICommand interface, and events implement the IEvent interface. (Some other messages, such as replies, implement the IMessage interface.) For many projects this is good enough, but you’ve always been able to override these conventions if you want to.

Here’s how you might currently identify messages based on a type name suffix:

endpointConfiguration.Conventions()
  .DefiningCommandsAs(t => t.FullName.EndsWith("Command"))
  .DefiningEventsAs(t => t.FullName.EndsWith("Event"))
  .DefiningMessagesAs(t => t.FullName.EndsWith("Message"));

The problem is that this overrides the current convention every time it is called, meaning that you can only have a single message convention that all messages must adhere to. As a system grows over time, or merges with other systems, message conventions are likely to shift and change. Dealing with that using a single message convention can lead to some complicated and tightly-coupled code. To handle these cases, we have introduced a new message convention abstraction. Here’s the same suffix message convention applied using this new abstraction.

class TypeNameSuffixConvention : IMessageConvention
{
  public string Name { get; } = "Type name suffix";
  public bool IsCommandType(Type t) => t.FullName.EndsWith("Command");
  public bool IsEventType(Type t) => t.FullName.EndsWith("Event");
  public bool IsMessageType(Type t) => t.FullName.EndsWith("Message");
}

endpointConfiguration.Conventions()
  .Add(new TypeNameSuffixConvention());

The Add(IMessageConvention) method doesn’t override the existing convention, so now the endpoint will recognize types that match either the built-in default convention (based on marker interfaces) or the type name suffix convention above. As the system grows over time and messages are added from other areas of the solution, you can define new message type conventions and add them to the list.

endpointConfiguration.Conventions()
  .Add(new SalesMessageConvention())
  .Add(new ShippingMessageConvention())
  .Add(new BillingMessageConvention());

If you need to know which message conventions are configured for an endpoint, this info has been added to the startup diagnostics file.

{
  // ...
  Messages: {
    CustomConventionsUsed: true,
    MessageConventions: [
      "NServiceBus Marker Interfaces",
      "Sales messages",
      "Shipping messages",
      "Billing messages"
    ]
  }
  // ...
}

If you need to know which convention was applied to each message type, turn on debug logging:

2020-07-20 13:20:01.576 DEBUG TestMultiConventions.PlaceOrder identified as message type by Sales messages convention.

🔗New conversations

A message conversation is a set of related messages. Each message sent by NServiceBus contains a header that states what conversation that message is a part of. This header is sticky. When a message is processed, the conversation header is copied to any outgoing messages. This is one of the key headers that is used to build the visualizations in ServiceInsight. All of the messages in a single diagram share the same conversation id.

ServiceInsight Flow Diagram

You’ve always been able to control the conversation id by setting a custom conversation id strategy for the endpoint or setting the header manually for an outgoing message. Both of these techniques only work for the very first message—the one that starts the conversation. Once that first message is sent, you can’t alter the conversation id for any subsequent messages as that would break the concept of the logical conversation—including the visualizations in ServiceInsight.

There are some scenarios where it does make sense to start a new logical conversation:

  • Sagas are sometimes used as schedulers to trigger some recurring task. In this case, every instance of the recurring task would be part of the same giant never-ending conversation.
  • An integration handler could load data from a data store and initiate a process for each record. It would make sense to start new conversations rather than having all of these messages be a part of the same massive conversation.

Now we have a declarative API for starting a new conversation:

public async Task Handle(UpdatePricing message, IMessageHandlerContext context)
{
    var pricingRecords = LoadPricingRecords();

    foreach(var record in pricingRecords)
    {
        var sendOptions = new SendOptions();
        sendOptions.StartNewConversation();

        await context.Send(new UpdatePrice(record.ProductId), sendOptions);
    }
}

This API clearly shows the intent of starting a new message conversation, but it also allows us to record the original conversation id using a new header NServiceBus.PreviousConversationId. In the future, we may upgrade ServiceInsight to use this header to show cross-conversation links.

🔗Summary

There are several other more minor improvements in NServiceBus version 7.4.0, mostly intended to guide you toward the pit of success by keeping you out of trouble, providing feedback through helpful exceptions, and making reporting more helpful for when things go wrong and you need to contact our support. You can find out all about them in the full release notes.

Our documentation has been updated for all of the new APIs. Where code snippets are used, you will see a new NServiceBus 7.4 option in the Switch Version dropdown button.

As always, NServiceBus version 7.4 is available on NuGet. Happy coding!

Share on Twitter

About the authors

Hadi Eskandari

Hadi Eskandari is a developer at Particular Software who, at his core, loves to contribute to open source projects.

Mike Minutillo

Mike Minutillo is an API artist at Particular Software who is always on the lookout for a cleaner way of expressing coding concepts.


  1. Message conventions determine what classes in an assembly are considered messages.

Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.