Custom mapping

Component: NHibernate Persistence | Nuget: NServiceBus.NHibernate (Version: 7.x)
Target NServiceBus Version: 6.x

Sometimes the database schema that gets generated by NServiceBus.NHibernate for the saga data types is insufficient. For example, storing strings with a different string length, want so use complex types, need to store DateTime values with a high precision or maybe want to tweak the eager versus lazy loading to improve performance. A custom mapping will be required to achieve this, so that the generated default mapping will not be used.

Custom mapping options:

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

There are probably even other options, but these are the most frequently used.

Which method to use depends on if mapping as xml, fluent code or code attributes. There can be small performance differences during startup but not when processing messages.

It is not required to create a custom mapping for all sagas if only one saga needs one. The NHibernate configuration in each sample type demonstrate how both generated and custom mappings can work simultaneously.
When creating a custom mapping then mapping the primary key and unique indexes must be done as part of that mapping.

When using custom mappings ensure unique indexes for all mapped saga properties are correctly mapped. Especially when using optimistic concurrency, a transaction isolation level different from serializable and allow concurrent processing of messages.

Not adding a unique constraint can result in duplicate saga entities as the second insert will not fail when inserting the same value when multiple messages are processed concurrently referencing the same saga instance.

Prerequisites

The samples rely on .\SQLEXPRESS and need 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

Edit
<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 mappings

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

Edit
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 gives a type-safe mapping approach where the mapping is specified as code (is not as .hbm.xml) but still separate from the classes. The benefit is compile time feedback when a mapping is not valid anymore when there are breaking changes in mappings but also that classes and mappings are not part of the same .NET type.

To use it with NServiceBus:

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

Example of a possible implementation:

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

Use NHibernate.Mapping.Attributes

With NHibernate.Mapping.Attributes saga data classes can be decorated. This keeps the classes, mapping and schema data very close. The saga types have a dependency on the NHibernate.Mapping.Attributes assembly.

NHibernate.Mapping.Attributes needs to know what types to scan to generate an NHibernate mapping configuration that gets passed to the 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:

Edit
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

The NHibernate Loquacious API is its native mapping by code. Declare the mapping in code just like FluentNHibernate but using a different syntax more close to its xml schema. It can help create a type-safe configuration but can also create the custom mappings. The benefit is that this API is already available via the NHibernate package so 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

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

Related Articles


Last modified