Sometimes, the database schema 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 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)
Other options exist, but these are the most frequently used. Minor variances in performance can exist between these options, 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, mapping the primary key and unique indexes is required.
When using a custom mapping, ensure a unique index exists for every column referenced by a .
saga mapping expression. Not adding a unique constraint can result in duplicate saga entities. 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 correctly.
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 .
content 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 add mappings from the file system using the following example:
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 invalid.
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 passing 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. Like FluentNhibernate, the mapping is declared in code, but it uses a different syntax that is more closely aligned to NHibernate's xml schema. NHibernate Loquacious can help create type-safe configurations and custom mappings. The benefit of using NHibernate Loquacious is that this API is already available via the NHibernate package, requiring 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;
}