Mapping by convention

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.

Getting ready

Complete the Getting ready instructions at the beginning of this chapter.

How to do it…

  1. Add a folder named MappingByConvention to the MappingRecipes project.
  2. Add a class named 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);
        }
      }
    }
  3. Add a new class named 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"
         } 
        }
       });
      }
     }
    }
  4. Run the application and start the MappingByConvention recipe.

How it works…

Convention mapping in NHibernate has two main components.

  • The model inspector: A model inspector, which is a class that implements the IModelInspector interface, is responsible for providing answers to questions that the mapping process asks about classes and properties:
    • Is this a class that should be mapped?
    • Is this collection property a list, a set or a bag?
    • Is this class part of an inheritance hierarchy?
    • Is this a version property?

    ...and many more.

  • The model mapper: A model mapper is an instance of the 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.

There's more…

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.55.18