Migrate handlers and sagas to Version 6

Component: NServiceBus

The handler method on IHandleMessages<T> now returns a Task. To leverage async code, add the async keyword to the handler method and use await for async methods. To convert the synchronous code add return Task.FromResult(0); or return Task.CompletedTask (.NET 4.6 and higher) to the handler methods.

Do not return null from the message handlers. A null will result in an Exception.
6.x NServiceBus
public class UpgradeMyAsynchronousHandler :
    IHandleMessages<MyMessage>
{
    public Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        return SomeLibrary.SomeAsyncMethod(message);
    }
}

public class UpgradeMySynchronousHandler :
    IHandleMessages<MyMessage>
{
    public Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        // when no asynchronous code is executed in a handler
        // Task.CompletedTask can be returned
        SomeLibrary.SomeMethod(message.Data);
        return Task.CompletedTask;
    }
}
5.x NServiceBus
public class UpgradeMessageHandler :
    IHandleMessages<MyMessage>
{
    public void Handle(MyMessage message)
    {
        SomeLibrary.SomeMethod(message.Data);
    }
}

API Changes

Bus Send and Receive

There is also a change in the parameters, giving access to the IMessageHandlerContext, which provides the methods that used to be called from IBus. Use the IMessageHandlerContext to send and publish messages.

6.x NServiceBus
public class SendAndPublishHandler :
    IHandleMessages<MyMessage>
{
    public async Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        await context.Send(new MyOtherMessage())
            .ConfigureAwait(false);
        await context.Publish(new MyEvent())
            .ConfigureAwait(false);
    }
}

Message handler ordering

In Version 6 the message handler ordering APIs are simplified. The full API can be seen in Handler ordering.

Specifying a Handler to run first

6.x NServiceBus
endpointConfiguration.ExecuteTheseHandlersFirst(typeof(HandlerB));
5.x NServiceBus
public class MySpecifyingFirst :
    ISpecifyMessageHandlerOrdering
{
    public void SpecifyOrder(Order order)
    {
        order.SpecifyFirst<HandlerB>();
    }
}

Specifying Handler order

6.x NServiceBus
endpointConfiguration.ExecuteTheseHandlersFirst(
    typeof(HandlerB),
    typeof(HandlerA),
    typeof(HandlerC));
5.x NServiceBus
busConfiguration.LoadMessageHandlers(First<HandlerB>.Then<HandlerA>().AndThen<HandlerC>());

New context arguments

The signature for the mutators now passes context arguments that give access to relevant information on the message and also the mutation the message. This context will give access to the same functionality as previous versions so just update the code accordingly.

See header manipulation for one example on how this might look.

HandleCurrentMessageLater and the Outbox

The HandleCurrentMessageLater method can no longer be used in conjunction with the Outbox.

When this scenario is detected an exception with the following message will be throw:

HandleCurrentMessageLater cannot be used in conjunction with the Outbox. Use the recoverability mechanisms or delayed delivery instead.

Use the recoverability or delayed delivery APIs instead when using the Outbox.

Migration

In previous versions of NServiceBus, a typical handler class looks like the below:

6.x NServiceBus
public class MigrationBeginning :
    IHandleMessagesFromPreviousVersions<MyMessage>
{
    public IBus Bus { get; set; }

    public void Handle(MyMessage message)
    {
        Bus.Send(new MyOtherMessage());
        Bus.Publish(new MyEvent());
    }
}

Implement the new IHandleMessages interface. Remove any code that is generated by the IDE or additional tools like Resharper and mark the new Handle method as async by adding the async keyword.

6.x NServiceBus
public class MigrationStep1 :
    IHandleMessages<MyMessage>
{
    public IBus Bus { get; set; }

    public void Handle(MyMessage message)
    {
        Bus.Send(new MyOtherMessage());
        Bus.Publish(new MyEvent());
    }

    public async Task Handle(MyMessage message, IMessageHandlerContext context)
    {
    }
}

Rename the bus property or the bus constructor parameter to context. Although the IBus interface has been deprecated and can be safely removed, for this step renaming the property instead of deleting it comes in useful when refactoring existing code. It's much easier to rename all the existing references from bus property to context.

6.x NServiceBus
public class MigrationStep2 :
    IHandleMessages<MyMessage>
{
    public IBus context { get; set; }

    public void Handle(MyMessage message)
    {
        context.Send(new MyOtherMessage());
        context.Publish(new MyEvent());
    }

    public async Task Handle(MyMessage message, IMessageHandlerContext context)
    {
    }
}

Inline or "cut-and-paste" the old Handle method code into the new asynchronous Handle method. When the code is compiled a compiler warning CS4014 will be shown indicating that there are asynchronous methods in the handler which are not awaited.

6.x NServiceBus
public class MigrationStep3 :
    IHandleMessages<MyMessage>
{
    public async Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        // CS4014: Consider applying the 'await' operator to the result of the call.
        context.Send(new MyOtherMessage());
        context.Publish(new MyEvent());
    }
}

Fix the compiler warnings by introducing the await statement followed by ConfigureAwait(false) to each asynchronous method call.

Visual Studio 2015 and higher has the capability to automatically fix those warnings with the Ctrl+. (depending on the keybindings) shortcut. It is even possible to fix it in the whole solution if desired.
6.x NServiceBus
public class MigrationStep4 :
    IHandleMessages<MyMessage>
{
    public async Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        await context.Send(new MyOtherMessage())
            .ConfigureAwait(false);
        await context.Publish(new MyEvent())
            .ConfigureAwait(false);
    }
}

After these steps start moving other code in the handler towards async if the code supports it when it is desired to fully leverage async/await. For example with Entity Framework instead of calling SaveChanges call SaveChangesAsync on the database context.

For information about how to migrate handlers with dependencies that access the IBus interface, refer to IBus interface has been deprecated guidance.

Saga API Changes

Remove NServiceBus.Saga namespace

The NServiceBus.Saga namespace has been removed to stop it clashing with the NServiceBus.Saga.Saga class. For all commonly used APIs (e.g., the Saga class and IContainSagaData interface) they have been moved into the NServiceBus namespace. Other more advanced APIs (e.g., the IFinder and IHandleSagaNotFound interfaces) have been moved into the NServiceBus.Sagas namespace.

In most cases using NServiceBus.Saga can be replaced with using NServiceBus.

Unique attribute no longer needed

NServiceBus will automatically make the correlated saga property unique without the need for an explicit [Unique] attribute to be used. This attribute can be safely removed from saga data types.

ConfigureHowToFindSaga

All messages that start the saga (IAmStartedByMessages<T>) need to be mapped to the saga data using either a mapping in ConfigureHowToFindSaga method, or a custom saga finder, otherwise an exception will be thrown on endpoint startup. Other messages that are handled by the saga (IHandleMessages<T>) also require mappings, unless they are reply messages resulting from a message sent out of the saga, in which case they will contain the SagaId in a message header. Messages that cannot be mapped by a SagaId message header, by a property mapping in ConfigureHowToFindSaga, or via a custom saga finder will throw a runtime exception.

In the below example, the OrderSaga is started by the StartOrder message. The OrderSaga also handles the CompleteOrder message.

6.x NServiceBus
public class OrderSaga :
        Saga<OrderSagaData>,
        IAmStartedByMessages<StartOrder>,
        IHandleMessages<CompleteOrder>

In Version 6, the StartOrder message will also need to be specified in the ConfigureHowToFindSaga method.

6.x NServiceBus
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
{
    mapper.ConfigureMapping<StartOrder>(message => message.OrderId)
        .ToSaga(sagaData => sagaData.OrderId);

    mapper.ConfigureMapping<CompleteOrder>(message => message.OrderId)
        .ToSaga(sagaData => sagaData.OrderId);
}

Correlating properties

Version 6 automatically correlates incoming message properties to its saga data counterparts. Any saga data correlation in the message handler code can be safely removed. Correlated properties (for existing saga instances) will not be changed once set.

Correlated properties must have a non default value, i.e. not null and not empty, assigned when persisted. If not the following exception will be thrown:

The correlated property 'MyPropery' on Saga 'MySaga' does not have a value.
A correlated property must have a non-default (i.e. non-null and non-empty) value assigned when a new saga instance is created.

Use a custom finder for the received message to override this validation.

6.x NServiceBus
public async Task Handle(StartOrder message, IMessageHandlerContext context)
{
    // The processing logic for the StartOrder message
}
5.x NServiceBus
public void Handle(StartOrder message)
{
    Data.OrderId = message.OrderId;
    // The processing logic for the StartOrder message
}

Versions 6 and above will only support correlating messages to a single saga property. Correlating on more than one property is still supported by creating a custom saga finder. If sagas with multiple correlations mappings to different properties are detected the following exception will be thrown:

Sagas can only have mappings that correlate on a single saga property. Use custom finders to correlate *message types* to Saga *saga type*

Saga persisters & finders

Saga persisters (ISagaPersister) and finders (IFindSagas) introduce a new parameter SagaPersistenceOptions. This parameter gives access to the saga metadata and pipeline context. This enables persisters and finders to manipulate everything that exists in the context during message pipeline execution. For more information see Sagas and Complex saga finding logic.

MarkAsComplete no longer virtual

The Saga base class method MarkAsComplete is no longer virtual.

RequestTimeout requires IMessageHandlerContext

RequestTimeout requires a IMessageHandlerContext as additional parameter. Pass the context argument received in the handle method to RequestTimeout.

ReplyToOriginator requires IMessageHandlerContext

ReplyToOriginator requires a IMessageHandlerContext as additional parameter. Pass the context argument received in the handle method to RequestTimeout.

Related Articles

  • Handlers
    Write a class to handle messages in NServiceBus.
  • Sagas
    NServiceBus uses event-driven architecture to include fault-tolerance and scalability in long-term business processes.

Last modified