Simple NHibernate Persistence Usage

NuGet Package:
NServiceBus.NHibernate 11.x
Target Version:
NServiceBus 10.x

Prerequisites

This sample requires an instance of SQL Server and a database named Samples.NHibernate. The database must already exist, as the endpoint creates only the tables it needs.

The Server project connects to localhost,1433 using SQL Server authentication. To run against SQL Server Express instead, use the .\SqlExpress connection string shown in the comment directly above the connectionString variable in Program.cs.

Code walk-through

This sample shows a simple client/server scenario.

  • Client sends a StartOrder message to Server.
  • Server starts an OrderSaga.
  • OrderSaga:
    • Sends a ShipOrder message to itself, and the handler for that message saves OrderShipped business data to the database.
    • Requests a timeout with CompleteOrder data.
  • When the CompleteOrder timeout fires, the OrderSaga publishes an OrderCompleted event.
  • Client handles the OrderCompleted event.

NHibernate config

NHibernate is configured with the right driver, dialect, and connection string. Then, since NHibernate needs a way to map the class to the database table, the configuration code does this using the ModelMapper API. Finally, the configuration is passed to the NServiceBus NHibernate persistence.

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

// for SqlExpress use Data Source=.\SqlExpress;Initial Catalog=Samples.NHibernate;Integrated Security=True;Max Pool Size=100;Encrypt=false
var connectionString = @"Server=localhost,1433;Initial Catalog=Samples.NHibernate;User Id=SA;Password=yourStrong(!)Password;Max Pool Size=100;Encrypt=false";
var hibernateConfig = new Configuration();
hibernateConfig.DataBaseIntegration(x =>
{
    x.ConnectionString = connectionString;
    x.Dialect<MsSql2012Dialect>();
    x.Driver<MicrosoftDataSqlClientDriver>();
});

AddMappings(hibernateConfig);

persistence.UseConfiguration(hibernateConfig);

Order saga data

Note that to use NHibernate's lazy-loading feature, all 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 readonly ILog log = LogManager.GetLogger<OrderSaga>();

    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.MapSaga(s => s.OrderId).ToMessage<StartOrder>(m => m.OrderId);
    }

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        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 uses the ISession instance 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

Data in the database is stored in two 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 with type varchar(255).
  • Custom properties on OrderSagaData, in this case OrderDescription and OrderId, are also mapped to columns with the same name and the respective types.

Query results for the OrderSagaData table

The handler stored data

Query results for the OrderShipped table

Related Articles

  • NHibernate Persistence
    NHibernate-based persistence for NServiceBus.
  • Persistence
    Features of NServiceBus requiring persistence include timeouts, sagas, and subscription storage.
  • Sagas
    Master NServiceBus sagas to coordinate distributed workflows and ensure reliable long-running processes.