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:
- 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.
- 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.
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;
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)
{
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)
{
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)
public override IEnumerable<IAutomappingStep> GetMappingSteps(AutoMapper mapper, FluentNHibernate.Conventions.IConventionFinder conventionFinder)
{
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.