NHibernate Custom Saga Finding Logic

Component: NHibernate Persistence
NuGet NServiceBus.NHibernate (7.x)
Target NServiceBus Version: 6.x

Code walk-through

When the default Saga message mappings do not satisfy the requirements, custom logic can be put in place to allow NServiceBus to find a saga data instance based on which logic best suites the environment.

NHibernate setup

This sample requires NHibernate persistence package and a running Microsoft SQL Server instance configured accordingly. The sample NHibernate setup can be configured according to the environment:

var persistence = endpointConfiguration.UsePersistence<NHibernatePersistence>();
persistence.ConnectionString(@"Data Source=.\SqlExpress;Database=NHCustomSagaFinder;Integrated Security=True");

The Saga

The saga shown in the sample is a very simple order management saga that:

  • Handles the creation of an order.
  • Offloads the payment process to a different handler.
  • Handles the completion of the payment process.
  • Completes the order.
public class OrderSaga :
    Saga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>,
    IHandleMessages<CompletePaymentTransaction>,
    IHandleMessages<CompleteOrder>
{
    static ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.ConfigureMapping<StartOrder>(_ => _.OrderId)
            .ToSaga(_=> _.OrderId);
        mapper.ConfigureMapping<CompleteOrder>(_ => _.OrderId)
            .ToSaga(_ => _.OrderId);
    }

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        Data.PaymentTransactionId = Guid.NewGuid().ToString();

        log.Info($"Saga with OrderId {Data.OrderId} received StartOrder with OrderId {message.OrderId}");
        var issuePaymentRequest = new IssuePaymentRequest
        {
            PaymentTransactionId = Data.PaymentTransactionId
        };
        return context.SendLocal(issuePaymentRequest);
    }

    public Task Handle(CompletePaymentTransaction message, IMessageHandlerContext context)
    {
        log.Info($"Transaction with Id {Data.PaymentTransactionId} completed for order id {Data.OrderId}");
        var completeOrder = new CompleteOrder
        {
            OrderId = Data.OrderId
        };
        return context.SendLocal(completeOrder);
    }

    public Task Handle(CompleteOrder message, IMessageHandlerContext context)
    {
        log.Info($"Saga with OrderId {Data.OrderId} received CompleteOrder with OrderId {message.OrderId}");
        MarkAsComplete();
        return Task.CompletedTask;
    }

}

From the process point of view it is important to note that the saga is not sending to the payment processor the order id, instead it is sending a payment transaction id. A saga can be correlated given more than one unique attribute, such as OrderId and PaymentTransactionId, requiring both to be treated as unique.

class CompletePaymentTransactionSagaFinder :
    IFindSagas<OrderSagaData>.Using<CompletePaymentTransaction>
{

    public Task<OrderSagaData> FindBy(CompletePaymentTransaction message, SynchronizedStorageSession storageSession, ReadOnlyContextBag context)
    {
        var session = storageSession.Session();
        var orderSagaData = session.QueryOver<OrderSagaData>()
            .Where(d => d.PaymentTransactionId == message.PaymentTransactionId)
            .SingleOrDefault();
        return Task.FromResult(orderSagaData);
    }

}

Building a saga finder requires to define a class that implements the IFindSagas<TSagaData>.Using<TMessage> interface. The class will be automatically picked up by NServiceBus at configuration time and used each time a message of type TMessage, that is expected to load a saga of type TSagaData, is received. The FindBy method will be invoked by NServiceBus.

In the sample the implementation of the ConfigureHowToFindSaga method, that is required, is empty since a saga finder is provided for each message type that the saga is handling. It is not required to provide a saga finder for every message type, a mix of standard saga mappings and custom saga finding is a valid scenario.

Related Articles

  • NHibernate Persistence
    NHibernate-based persistence for NServiceBus.
  • Sagas
    NServiceBus uses event-driven architecture to include fault-tolerance and scalability in long-term business processes.

Last modified