Simple NHibernate Persistence Usage

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

Prerequisites

The samples rely on .\SqlExpress and need the database 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 a CompleteOrder data.
  • When the CompleteOrder timeout fires the OrderSaga publishes a OrderCompleted event.
  • The Server then publishes a message that the client has subscribed to.
  • Client handles OrderCompleted event.

NHibernate Config

Configure NHibernate with the right driver, dialect and connection string. Since NHibernate needs a form of mapping of 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 Data in 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 the SagaData, in this case OrderDescription and OrderId, are also mapped to columns with the same name and of 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 a 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