Click here to Skip to main content
15,885,757 members
Articles / Programming Languages / C# 4.0

Fluent NHibernate Automapping for Unidirectional ManyToMany relationship

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
27 Jan 2012CPOL4 min read 19.6K   1   7  
Fluent NHibernate Automapping for Unidirectional ManyToMany relationship

Introduction

Fluent Nhibernate framework is a great framework, where you can specify strongly typed mapping in the form of classes on the top of NHibernate framework.

While a developer can specify his own mapping for each entity explicitly fluent nhibernate also provides the option for you to do an automapping where the mapping is automaticaly created according to some rules you teach it to fluent nhibernate (fluent nhibernate conventions) and therefore you will end up that straight forward cases doesnt require you to create there mapping, in my case I have so many lookups in the system that Automapping will be a hell of time saver for me.

In the other hand I also support explicit mapping side by side with automapping, I think its necessary since we always face these exceptional and rare relations that our automapping cant handle, of course fluent nhibernate overrides are an option to over come these exceptional cases but I am not pretty much of a supporter to it since I think it might scatter your mappings with many overrides each time you face one.

Background

Creating Fluent Nhibernate automapping was pretty much straight forward in most cases, but when it comes to ManyToMany relationship it aint that easy, and I really faced hard time making this one work as I wanted, since a little bit of concept difference has been there in between me and Fluent Nhibernate.

My case was as far as I consider is a common case to happen, a Unidirectional ManyToMany relationship, Customer has a List of Product, while product has no reference of what so ever to Customer since its used by many customers, of course such a relation will immediatley requires us to ask: for this relationship to be built on the database, is it a foriegn key or a conjunction table? in other words is its a OneToMany relationship (foriegn key) or ManyToMany relationship (using a conjunction table to connect both tables)?

Fluent Nhibernate considers this relationship immediately as a OneToMany relationship, so it uses the HasMany convention class in stead of HasManyToMany convention class for the ManyToMany relationship, actually for me that was disappointing !!! how would he decide such a relationship to be considered as OneToMany while in most cases this should be treated as ManyToMany since its one way and I would assume I will use a conjunction table in such a case!!! at least he should give me the option to choose which one to use: HasMany or ManyToMany.

Using the code

In order to make things work for me I decided to force fluent nhibernate to understand that this relation is a Unidirectionaly ManyToMany relationship instead of HasMany relationship. to do that I had to do the following:

  1. Create a custom Automapping step for the HasManyToMany relationship: fluent nhibernate uses Steps (not sure what the concept of the naming is for .. anyway) which are classes inherit from IAutomappingStep for all possible types of relationships, it investigates all possible relations and put them agains these steps, if a relation is considered fit to this step then it maps the relation according to this step, so this custom Step shall be used instead of the existing one in order to make it understand that this relation (the unidirectional manytomay) is a true manytomany relationship.
  2. Replace the existing ManyToManyStep with the modified one

Actually understanding the above points is the most important part, since we now know how fluent nhibernate works toward the case we want to understand we can do the work.

C#
public class CustomHasManyToManyStep : IAutomappingStep
    {
        private readonly IAutomappingConfiguration cfg;

        public CustomHasManyToManyStep(IAutomappingConfiguration cfg)
        {
            this.cfg = cfg;
        }

        public bool ShouldMap(Member member)
        {
            var type = member.PropertyType;
            if (type.Namespace != "Iesi.Collections.Generic" &&
                type.Namespace != "System.Collections.Generic")
                return false;
            if (type.HasInterface(typeof(IDictionary)) || type.ClosesInterface(typeof(IDictionary<,>)) || type.Closes(typeof(System.Collections.Generic.IDictionary<,>)))
                return false;

            //SamerA: Tweak to allow unidirectional ManyToMany mapping along with the Bidirectional            
            return (IsBidirectionalManyToMany(member) || IsUnidirectionalManyToMany(member));
        }

        static Member GetInverseCollectionProperty(Member member)
        {
            var type = member.PropertyType;
            var expectedInversePropertyType = type.GetGenericTypeDefinition()
                .MakeGenericType(member.DeclaringType);

            var argument = type.GetGenericArguments()[0];
            return argument.GetProperties()
                .Select(x => x.ToMember())
                .Where(x => x.PropertyType == expectedInversePropertyType && x != member)
                .FirstOrDefault();
        }

        static bool IsBidirectionalManyToMany(Member member)
        {
            var type = member.PropertyType;
            var expectedInversePropertyType = type.GetGenericTypeDefinition()
                .MakeGenericType(member.DeclaringType);

            var argument = type.GetGenericArguments()[0];
            return argument.GetProperties()
                .Select(x => x.ToMember())
                .Any(x => x.PropertyType == expectedInversePropertyType && x != member);
        }

        static bool IsUnidirectionalManyToMany(Member member)
        {
            var type = member.PropertyType;

            var argument = type.GetGenericArguments()[0];
            return argument.GetProperties()
                .Select(x => x.ToMember()).All(x => x.PropertyType != member.DeclaringType && x != member);
        }

        public void Map(ClassMappingBase classMap, Member member)
        {
            var inverseProperty = GetInverseCollectionProperty(member);

            var parentSide = inverseProperty == null ? member.DeclaringType : cfg.GetParentSideForManyToMany(member.DeclaringType, inverseProperty.DeclaringType);

            var mapping = GetCollection(member);

            ConfigureModel(member, mapping, classMap, parentSide);

            classMap.AddCollection(mapping);
        }

        static CollectionMapping GetCollection(Member property)
        {
            var collectionType = CollectionTypeResolver.Resolve(property);

            return CollectionMapping.For(collectionType);
        }

        void ConfigureModel(Member member, CollectionMapping mapping, ClassMappingBase classMap, Type parentSide)
        {
            // TODO: Make the child type safer
            mapping.SetDefaultValue(x => x.Name, member.Name);
            mapping.Relationship = CreateManyToMany(member, member.PropertyType.GetGenericArguments()[0], classMap.Type);
            mapping.ContainingEntityType = classMap.Type;
            mapping.ChildType = member.PropertyType.GetGenericArguments()[0];
            mapping.Member = member;

            SetDefaultAccess(member, mapping);
            SetKey(member, classMap, mapping);

            if (parentSide != member.DeclaringType)
                mapping.Inverse = true;
        }

        void SetDefaultAccess(Member member, CollectionMapping mapping)
        {
            var resolvedAccess = MemberAccessResolver.Resolve(member);

            if (resolvedAccess != Access.Property && resolvedAccess != Access.Unset)
            {
                // if it's a property or unset then we'll just let NH deal with it, otherwise
                // set the access to be whatever we determined it might be
                mapping.SetDefaultValue(x => x.Access, resolvedAccess.ToString());
            }

            if (member.IsProperty && !member.CanWrite)
                mapping.SetDefaultValue(x => x.Access, cfg.GetAccessStrategyForReadOnlyProperty(member).ToString());
        }

        ICollectionRelationshipMapping CreateManyToMany(Member property, Type child, Type parent)
        {
            var mapping = new ManyToManyMapping
            {
                Class = new TypeReference(property.PropertyType.GetGenericArguments()[0]),
                ContainingEntityType = parent
            };

            mapping.AddDefaultColumn(new ColumnMapping { Name = child.Name + "_id" });

            return mapping;
        }

        void SetKey(Member property, ClassMappingBase classMap, CollectionMapping mapping)
        {
            var columnName = property.DeclaringType.Name + "_id";
            var key = new KeyMapping();

            key.ContainingEntityType = classMap.Type;
            key.AddDefaultColumn(new ColumnMapping { Name = columnName });

            mapping.SetDefaultValue(x => x.Key, key);
        }
    }

Points of Interest

Now looking for the above code the most important things that we need to have a look to are:

1. IsBirectionalManyToMany and IsUnidirectionalManyToMany methods: the first one determines whether this is a full many to many relationship by seeing if there is a counter list on the other side for the current class. while IsUnidirectionalManyToMany checks if there is NO List or Property in the other side for the current class, this is where we consider this as a ManyToMany relationship.

2. The Map method: now after we agreed that this is a UnidirectionalManytoMany relationship by knowing how and returning True to ShouldMap method, we need to do the mapping, for the unidreictional relationship the how to map will be the same as a bidirectional one except that the parent side of the relationship is always fixed in the case of unidirectional relationship since the other side has no idea of this relationship of any reference.

Finally is replacing the existing step with our custom step to enable this relation that we have fixed, this can be acheived in your Default Automapping Configuration class you have (which inherits from DefaultAutomappingConfiguration)

C#
public override IEnumerable<IAutomappingStep> GetMappingSteps(AutoMapper mapper, FluentNHibernate.Conventions.IConventionFinder conventionFinder)
        {
            //SamerA: Replace the original HasManyToManyStep with the Customized HasManyToManyStep to allow Unidirectional ManyToMany
            //Mapping when the other side has no reference for the current side.
            //NOTE: an alternative to the below code is to redefine all steps with the CustomHasManyToManyStep here, the only preference is use
            //the already created steps instead of creating new ones along with the old ones.

            //Get all defined steps
            var steps = base.GetMappingSteps(mapper, conventionFinder).ToList();

            var index = steps.FindIndex(x => x.GetType() == typeof(HasManyToManyStep));
            steps.RemoveAt(index);
            steps.Insert(index, new CustomHasManyToManyStep(this));

            return steps;
        }

The code is fairly simple here, we find the original one, remove it and insert the new custom one and then return the new modified list.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Jordan Jordan
Samer is a Computer and Digital Electronics Engineer who lives in Abu Dhabi UAE, worked on a fair number of personal projects as part of fun or freelancing, mostly related to integration between hardware and software (e.g Security Systems, Sensors, Cameras, Bluetooth ... etc), which gave him a powerful knowledge in this area and the ability to invent various complex designs to adhere for external elements effect those systems.

Through his career path he worked on a fair number of enterprise projects with well known software companies, which showed him the various aspects of an enterprise applications.

You may contact Samer through his e-mail: SamerX@outlook.com or Twitter: SamerX as well.

Comments and Discussions

 
-- There are no messages in this forum --