Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

NServiceBus.NHibernate custom saga mapping sample

NuGet Package: NServiceBus.NHibernate (10.x)
Target Version: NServiceBus 9.x

Sometimes the database schema that is generated by NServiceBus.NHibernate is insufficient for saga data types. For example, storing strings with a different string length, wanting to use complex types, needing to store DateTime values with a high precision, or wanting to tweak the eager versus lazy loading rules might be better options. A custom mapping will be required to achieve these changes.

Custom mappings can be added by:

  • Creating a *.hbm.xml file for the saga data class
  • Fluent NHibernate (popular separate fluent API)
  • NHibernate.Mapping.Attributes
  • Loquacious Configuration (native fluent API)

There are other options, but these are the most frequently used. The performance difference between these options can have small variances, but those will occur during endpoint startup, not when the endpoint is processing messages.

When using a custom mapping, ensure that a unique index exists for every column referenced by a .ToSaga() saga mapping expression. Not adding a unique constraint can result in duplicate saga entities as the second insert will not fail when inserting the same value if multiple messages are processed concurrently and they reference the same saga instance.

Prerequisites

This sample requires an instance of SQL Server at .\SqlExpress and the database Samples.CustomNhMappings to run properly.

Custom .hbm.xml mapping

Using NHibernate mapping files is the native way to customize the mappings. The mapping files are xml files that are either embedded as a resource in the assembly or available on the file system.

For more information, see: how to create a simple NHibernate based application.

The Mapping

The .hbm.xml contents is as follows

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Sample.XmlMapping">

  <class name="OrderSagaDataXml">
    <id name="Id"
        type="Guid"
        column="Id">
      <generator class="assigned" />
    </id>
    <version name="Version"/>
    <property name="Originator" />
    <property name="OriginalMessageId" />
    <property name="OrderId"
              type="AnsiString"
              length="100"
              unique="true"
              not-null="true" />
    <many-to-one name="From"
                 cascade="all-delete-orphan"
                 column="FromLocation"/>
    <many-to-one name="To"
                 cascade="all-delete-orphan"
                 column="ToLocation"/>
    <component name="Total">
      <property name="Currency"
                type="AnsiString"
                column="Total_Currency"
                length="3" />
      <property name="Amount"
                column="Total_Amount"/>
    </component>
  </class>

  <class name="OrderSagaDataXml+Location"
         table="OrderSagaDataXml_Location">
    <id name="Id"
        type="Guid">
      <generator class="guid.comb"></generator>
    </id>
    <property name="Lat"/>
    <property name="Long"/>
  </class>

</hibernate-mapping>

Reading the mapping file

Create a custom NHibernate configuration object and use the following example to add mappings from the file system.

static void AddMappingsFromFilesystem(Configuration nhConfiguration)
{
    var directory = Directory.GetCurrentDirectory();
    var hmbFiles = Directory.GetFiles(directory, "*.hbm.xml", SearchOption.TopDirectoryOnly);

    foreach (var file in hmbFiles)
    {
        nhConfiguration.AddFile(file);
    }
}

Use Fluent NHibernate

Fluent NHibernate provides a type-safe mapping approach where the mapping is specified in code (not as .hbm.xml) but the mapping is still separate from the classes. The benefit of this approach is the compile time feedback provided when a mapping is not valid.

To use it with NServiceBus:

  1. Install the FluentNHibernate package via NuGet.
  2. Create a custom NHibernate configuration
    • via FluentNHibernate
    • or by creating a new Configuration instance and pass it to FluentNHibernate
  3. Pass it to the NServiceBus NHibernate configuration.

Example of a possible implementation:

static Configuration AddFluentMappings(Configuration nhConfiguration)
{
    return Fluently
        .Configure(nhConfiguration)
        .Mappings(cfg =>
        {
            cfg.FluentMappings.AddFromAssemblyOf<OrderSagaDataFluent>();
        })
        .BuildConfiguration();
}

Use NHibernate.Mapping.Attributes

Saga data classes can be decorated with NHibernate.Mapping.Attributes. Saga types will have a dependency on the NHibernate.Mapping.Attributes assembly, but this keeps the classes, mapping and schema data very close.

NHibernate.Mapping.Attributes needs to know what types to scan to generate an NHibernate mapping configuration that can be passed to the NServiceBus NHibernate configuration.

  1. Add the NuGet package NHibernate.Mapping.Attributes
  2. Create a custom NHibernate configuration object.
  3. Initialize the attribute mapping (see sample below).
  4. Pass it to the NServiceBus NHibernate configuration.

Initialize the NHibernate attribute based mappings:

static void AddAttributeMappings(Configuration nhConfiguration)
{
    var hbmSerializer = new HbmSerializer
    {
        Validate = true
    };

    using (var stream = hbmSerializer.Serialize(typeof(Program).Assembly))
    {
        nhConfiguration.AddInputStream(stream);
    }
}

Use the Loquacious mapping by code API

NHibernate Loquacious API is a native mapping for NHibernate that is provided via code. Just like FluentNhibernate, the mapping is declared in code, but it uses a different syntax more closely aligned to NHibernate's xml schema. NHibernate Loquacious can help create a type-safe configuration, and it can also create custom mappings. The benefit of using NHibernate Loquacious is that this API is already available via the NHibernate package so it requires no additional downloads.

To use it:

  1. Create a custom NHibernate configuration object.
  2. Use either the model mapping or convention mapping features.
  3. Pass it to the NServiceBus NHibernate configuration.

Initialize NHibernate Loquacious configuration:

static Configuration AddLoquaciousMappings(Configuration nhConfiguration)
{
    var mapper = new ModelMapper();
    mapper.AddMappings(typeof(OrderSagaDataLoquacious).Assembly.GetTypes());
    nhConfiguration.AddMapping(mapper.CompileMappingForAllExplicitlyAddedEntities());
    return nhConfiguration;
}

Related Articles