In large object models, you will notice that many aspects of the mappings are repetitive. Maybe the Comb generator should generate all POIDs or all properties referencing a class without an Id property should be treated as a component mapping. By setting up mapping conventions, you can potentially avoid all explicit mappings and instead let the code structure do the work for you.
MappingByConvention
to the MappingRecipes
project.MyModelMapper
to the folder:using System.Collections.Generic; using System.Linq; using System.Reflection; using NH4CookbookHelpers.Mapping.Model; using NHibernate.Mapping.ByCode; namespace MappingRecipes.MappingByConvention { public class MyModelMapper : ConventionModelMapper { public MyModelMapper() { IsEntity((t, declared) => typeof(Entity).IsAssignableFrom(t) && typeof(Entity) != t); IsRootEntity((t, declared) => t.BaseType == typeof(Entity)); IsList((member, declared) => member .GetPropertyOrFieldType() .IsGenericType && member .GetPropertyOrFieldType() .GetGenericInterfaceTypeDefinitions() .Contains(typeof(IList<>))); IsVersion((member, declared) => member.Name == "Version" && member.MemberType == MemberTypes.Property && member.GetPropertyOrFieldType() == typeof(int)); IsTablePerClassHierarchy((t, declared) => typeof(Product).IsAssignableFrom(t)); BeforeMapSubclass += ConfigureDiscriminatorValue; BeforeMapClass += ConfigureDiscriminatorColumn; BeforeMapClass += ConfigurePoidGenerator; BeforeMapList += ConfigureListCascading; } private void ConfigureListCascading( IModelInspector modelInspector, PropertyPath member, IListPropertiesMapper propertyCustomizer) { propertyCustomizer.Cascade(Cascade.All | Cascade.DeleteOrphans); } private void ConfigurePoidGenerator( IModelInspector modelInspector, System.Type type, IClassAttributesMapper classCustomizer) { classCustomizer.Id(id => id.Generator(Generators.GuidComb)); } private void ConfigureDiscriminatorColumn( IModelInspector modelInspector, System.Type type, IClassAttributesMapper classCustomizer) { if (modelInspector.IsTablePerClassHierarchy(type)) { classCustomizer.Discriminator(x => x.Column(type.Name + "Type")); } } private void ConfigureDiscriminatorValue( IModelInspector modelInspector, System.Type type, ISubclassAttributesMapper subclassCustomizer) { subclassCustomizer.DiscriminatorValue(type.Name); } } }
Recipe
to the folder:using System.Collections.Generic; using System.Linq; using NH4CookbookHelpers.Mapping; using NH4CookbookHelpers.Mapping.Model; using NHibernate; using NHibernate.Cfg; namespace MappingRecipes.MappingByConvention { public class Recipe : BaseMappingRecipe { protected override void Configure(Configuration cfg) { var mapper = new MyModelMapper(); var mapping = mapper .CompileMappingFor(typeof(Product).Assembly .GetExportedTypes() .Where(x => x.Namespace == typeof(Product).Namespace)); cfg.AddMapping(mapping); } protected override void AddInitialData(ISession session) { session.Save(new Movie { Name = "Mapping by convention - the movie", Description = "An interesting documentary", UnitPrice = 300, Actors = new List<ActorRole> { new ActorRole { Actor = "NHibernate", Role = "The mapper" } } }); } } }
MappingByConvention
recipe.Convention mapping in NHibernate has two main components.
IModelInspe
ctor
interface, is responsible for providing answers to questions that the mapping process asks about classes and properties:...and many more.
ModelMapper
class or a class deriving from it. It uses an assigned IModelInspector
to investigate the entity classes and builds a HbmMapping
instance. While it does that, it fires off a set of events, such as BeforeMapClass
and AfterMapProperty
, at different stages of the process. By adding handlers to these events, we can influence the outcome.In the recipe, we created our own ModelMapper
, by deriving it from ConventionModelMapper
. This base class is convenient since it provides a set of useful defaults, and allows us to configure both the model inspection and the event handlers in the same place.
Our MyModelMapper
starts by defining the classes that should be included in the mapping:
IsEntity((t, declared) => typeof(Entity).IsAssignableFrom(t) && typeof(Entity) != t);
We want all classes deriving from Entity
to be included, but not Entity
itself.
Next, we define which classes are root
classes that are they are not subclasses of other mapped classes. In our example, Product
is such a class, but Movie
is not:
IsRootEntity((t, declared) => t.BaseType == typeof(Entity));
This will only return true
for classes directly deriving from Entity
. Movie
and Book
derives from Product
, so they will not be considered root
entities.
Next, comes a slightly more advanced check, which is supposed to identify whether a property (or a field) is an IList<T>
and subsequently should be mapped as a <list>
:
IsList((member, declared) => member .GetPropertyOrFieldType() .IsGenericType && member .GetPropertyOrFieldType() .GetGenericInterfaceTypeDefinitions() .Contains(typeof(IList<>)));
The IsVersion
call should be rather self-explanatory. Any integer property named Version will be mapped as a version property:
IsVersion((member, declared) => member.Name == "Version" && member.MemberType == MemberTypes.Property && member.GetPropertyOrFieldType() == typeof(int));
In order to specify that Book
and Movie
should be mapped as subclasses of Product
, sharing the same table, we answer the following question:
IsTablePerClassHierarchy((t, declared) => typeof(Product).IsAssignableFrom(t));
Note that we return true
for the base class itself.
Next come the event handlers and we set them up as shown:
BeforeMapSubclass += ConfigureDiscriminatorValue; BeforeMapClass += ConfigureDiscriminatorColumn; BeforeMapClass += ConfigurePoidGenerator; BeforeMapList += ConfigureListCascading;
Note the +=
operator, typical for events. We can add as many handlers to an event as we want and they will be invoked in the order they were added to the specific event. This makes it easy to keep the different handlers short and to the point. For example, in this recipe we have separated ConfigureDiscriminatorColumn
and ConfigurePoidGenerator
. Both are handlers for BeforeMapClass
, but since they handle completely different aspects, we keep them separate.
Once we have set up our model mapper (and inspector), all we need to create and add the mappings are:
var mapper = new MyModelMapper(); var mapping = mapper.CompileMappingFor(typeof(Product).Assembly .GetExportedTypes() .Where(x => x.Namespace == typeof(Product).Namespace)); cfg.AddMapping(mapping);
Actually, this specific case was a bit more convoluted than usually necessary, since we only wanted the classes in the same namespace as Product
. CompileMappingFor
simply takes an enumerable of classes that it should include in the mapping.
The model-mapping infrastructure is sufficiently powerful to accommodate almost any kind of conventions and customizations you may want to use. Consider, for instance, how to distinguish between the collections that should be mapped as <set>
and the ones that should be mapped as <bag>
or <list>
. Our implementation of IsSet
, IsBag
, and IsList
can check the property type (for example, mapping all ISet<T>
as a <set>
), but it can also use a property name convention, or perhaps even some kind of IsSetAttribute
, applied to the property.
In a similar way, we could decide whether an instance should cascade changes to related entities, perhaps based on whether those entities belong to the same aggregate (root owning entity).
The event handlers we used were all of the before kind. These could be called default conventions, since they are applied before any implicit or explicit customizations (such as those inside the ClassMappings
we created in the previous recipe) are performed. You can also add after handlers. They are applied at the very end, and can override any choices made. We could therefore call these enforced conventions.
52.15.55.18