Simple NHibernate Persistence Usage

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

Prerequisites

The sample relies on .\SqlExpress and the existence of a database named Samples.NHibernate to run properly.

Code walk-through

This sample shows a simple client/server scenario.

  • Client sends a StartOrder message to Server.
  • Server starts an OrderSaga.
  • OrderSaga requests a timeout with CompleteOrder data.
  • When the CompleteOrder timeout fires, the OrderSaga publishes a OrderCompleted event.
  • Server then publishes a message that the client has subscribed to.
  • Client handles the OrderCompleted event.

NHibernate config

Configure NHibernate with the right driver, dialect, and connection string. Since NHibernate needs a way to map the class to the database table, the configuration code also does that with ModelMapper API. Finally, the configuration is used to run the endpoint.

var endpointConfiguration = new EndpointConfiguration("Samples.NHibernate.Server");
var persistence = endpointConfiguration.UsePersistence<NHibernatePersistence>();

var nhConfig = new Configuration();
nhConfig.SetProperty(Environment.ConnectionProvider, "NHibernate.Connection.DriverConnectionProvider");
nhConfig.SetProperty(Environment.ConnectionDriver, "NHibernate.Driver.Sql2008ClientDriver");
nhConfig.SetProperty(Environment.Dialect, "NHibernate.Dialect.MsSql2008Dialect");
nhConfig.SetProperty(Environment.ConnectionStringName, "NServiceBus/Persistence");

AddMappings(nhConfig);

persistence.UseConfiguration(nhConfig);

Order saga data

Note that to use NHibernate's lazy-loading, all the properties on the saga data class must be virtual.

public class OrderSagaData :
    ContainSagaData
{
    public virtual Guid OrderId { get; set; }
    public virtual string OrderDescription { get; set; }
}

Order saga

public class OrderSaga :
    Saga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>,
    IHandleTimeouts<CompleteOrder>
{
    static ILog log = LogManager.GetLogger<OrderSaga>();

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

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        Data.OrderId = message.OrderId;
        var orderDescription = $"The saga for order {message.OrderId}";
        Data.OrderDescription = orderDescription;
        log.Info($"Received StartOrder message {Data.OrderId}. Starting Saga");

        var shipOrder = new ShipOrder
        {
            OrderId = message.OrderId
        };

        log.Info("Order will complete in 5 seconds");
        var timeoutData = new CompleteOrder
        {
            OrderDescription = orderDescription
        };

        return Task.WhenAll(
            context.SendLocal(shipOrder),
            RequestTimeout(context, TimeSpan.FromSeconds(5), timeoutData)
        );
    }

    public Task Timeout(CompleteOrder state, IMessageHandlerContext context)
    {
        log.Info($"Saga with OrderId {Data.OrderId} completed");
        var orderCompleted = new OrderCompleted
        {
            OrderId = Data.OrderId
        };
        MarkAsComplete();
        return context.Publish(orderCompleted);
    }
}

Handler using ISession

The handler access the ISession to store business data.

public class ShipOrderHandler :
    IHandleMessages<ShipOrder>
{
    public Task Handle(ShipOrder message, IMessageHandlerContext context)
    {
        var session = context.SynchronizedStorageSession.Session();
        var orderShipped = new OrderShipped
        {
            Id = message.OrderId,
            ShippingDate = DateTime.UtcNow,
        };

        session.Save(orderShipped);

        return Task.CompletedTask;
    }
}

The database

The data in the database is stored in three different tables.

The saga data

  • IContainSagaData.Id maps to the OrderSagaData primary-key and unique identifier column Id.
  • IContainSagaData.Originator and IContainSagaData.OriginalMessageId map to columns of the same name of type varchar(255).
  • Custom properties on SagaData, in this case OrderDescription and OrderId, are also mapped to columns with the same name and the respecting types.

The timeouts

  • The subscriber is stored in Destination column and includes Queue and Machine information.
  • The endpoint that initiated the timeout is stored in the Endpoint column.
  • The connected saga ID is stored in a SagaId column.
  • The serialized data for the message is stored in a State column.
  • The scheduled timestamp for the timeout is stored in a Time column.
  • Any headers associated with the timeout are stored in an array of key value pairs stored in the 'Headers' column.

The subscriptions

Note that the message type maps to multiple subscriber endpoints.

  • The Subscription message type and version are stored in the MessageType column.
  • The list of subscribers is stored in an array of objects each containing Queue and MachineName information.

The handler stored data

Related Articles

  • Persistence
    Features of NServiceBus requiring persistence include timeouts, sagas, and subscription storage.
  • Sagas
    NServiceBus uses event-driven architecture to include fault-tolerance and scalability in long-term business processes.

Last modified