NHibernate Custom Saga Finding Logic

Component: NHibernate Persistence
NuGet Package 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.

Prerequisites

An instance of SQL Server Express is installed and accessible as .\SqlExpress.

At startup each endpoint will create its required SQL assets including databases, tables and schemas.

The database created by this sample is NsbSamplesNhCustomSagaFinder.

NHibernate setup

This sample uses the NHibernate persistence which is configured as follows:

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

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