Sometimes the database schema that is generated by NServiceBus.
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
*.
file for the saga data classhbm. xml - 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.
There is no requirement to create a custom mapping for all sagas if a custom mapping is needed by only one saga.
When creating a custom mapping, the mapping of the primary key and unique indexes must be included.
When using a custom mapping, ensure that a unique index exists for every column referenced by a .
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 .
and the database Samples.
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.
Mapping
The .
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>
Configuration
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 .
) 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:
- Install the
FluentNHibernate
package via NuGet. - Create a custom NHibernate configuration
- via FluentNHibernate
- or by creating a new Configuration instance and pass it to FluentNHibernate
- Pass it to the NServiceBus NHibernate configuration.
Mapping
public class OrderSagaDataFluentMap :
ClassMap<OrderSagaDataFluent>
{
public OrderSagaDataFluentMap()
{
Id(x => x.Id)
.GeneratedBy
.Assigned();
Map(x => x.OriginalMessageId);
Map(x => x.Originator);
Map(x => x.OrderId)
.CustomType("AnsiString")
.Length(100)
.Not.Nullable()
.Unique();
Version(x => x.Version);
References(x => x.From, "FromLocation")
.Cascade.All();
References(x => x.To, "ToLocation")
.Cascade.All();
Component(x => x.Total, c =>
{
c.Map(x => x.Amount);
c.Map(x => x.Currency).Length(3).CustomType("AnsiString");
});
}
}
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.
- Add the NuGet package
NHibernate.
Mapping. Attributes - Create a custom NHibernate configuration object.
- Initialize the attribute mapping (see sample below).
- Pass it to the NServiceBus NHibernate configuration.
Mapping
[Class]
public class OrderSagaDataAttributes :
IContainSagaData
{
[Id(Name = "Id")]
public virtual Guid Id { get; set; }
[Property]
public virtual string OriginalMessageId { get; set; }
[Property]
public virtual string Originator { get; set; }
[Property(Length = 100, Type = "AnsiString", Unique = true)]
public virtual string OrderId { get; set; }
[Version]
public virtual int Version { get; set; }
[ManyToOne(Cascade = "all-delete-orphan", Column = "FromLocation")]
public virtual Location From { get; set; }
[ManyToOne(Cascade = "all-delete-orphan", Column = "ToLocation")]
public virtual Location To { get; set; }
public virtual AmountInfo Total { get; set; }
[Class(Table = "OrderSagaDataAttributes_Location")]
public class Location
{
[Id(Name = "Id", Generator = "guid.comb")]
public virtual Guid Id { get; set; }
[Property]
public virtual double Lat { get; set; }
[Property]
public virtual double Long { get; set; }
}
[Component(Name = "Total")]
public class AmountInfo
{
[Property(Type = "AnsiString", Length = 3, Column = "Total_Currency")]
public virtual string Currency { get; set; }
[Property(Column = "Total_Amount")]
public virtual decimal Amount { get; set; }
}
}
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:
- Create a custom NHibernate configuration object.
- Use either the model mapping or convention mapping features.
- Pass it to the NServiceBus NHibernate configuration.
Mapping
public class OrderSagaDataLoquaciousMap :
ClassMapping<OrderSagaDataLoquacious>
{
public OrderSagaDataLoquaciousMap()
{
Id(x => x.Id, m => m.Generator(Generators.Assigned));
Property(x => x.OriginalMessageId);
Property(x => x.Originator);
Property(x => x.OrderId, m =>
{
m.Unique(true);
m.Length(100);
m.NotNullable(true);
m.Type(NHibernateUtil.AnsiString);
});
Version(x => x.Version, m => { });
ManyToOne(x => x.From, m =>
{
m.Column("FromLocation");
m.Cascade(Cascade.All | Cascade.DeleteOrphans);
});
ManyToOne(x => x.To, m =>
{
m.Column("ToLocation");
m.Cascade(Cascade.All | Cascade.DeleteOrphans);
});
Component(x => x.Total, c =>
{
c.Property(x => x.Currency, m =>
{
m.Length(3);
m.Type(NHibernateUtil.AnsiString);
});
c.Property(x => x.Amount);
});
}
}
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;
}