Saturday, December 18, 2010

Intelligent brownfield mapping system on Fluent NHibernate




The code below tackles the flaw found by Stefan Steinegger when mapping multiple references. It's working robustly now, and compared to vanilla FNH, FNH.BF mapper won't let silent errors creep in, e.g. it won't let you silently misconfigure ambiguous references, it fail fast when there's ambiguity

The problem being solved by the brownfield mapping system: http://www.ienablemuch.com/2010/12/brownfield-system-problem-on-fluent.html


using System;

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

using NHibernate;
using NHibernate.Dialect;

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;
using FluentNHibernate.Conventions.Helpers;
using FluentNHibernate.Mapping;
using FluentNHibernate.Mapping.Providers;


namespace FluentNHibernate.BrownfieldSystem
{
    public class ClassMapExt<T> : ClassMap<T>
    {
        public IList<IManyToOneMappingProvider> ExtReferences { get { return this.references; } }
        public IList<ICollectionMappingProvider> ExtCollections { get { return this.collections; } }
    }

    public static class BrownfieldSystemHelper
    {


        public static T AddBrownfieldConventions<T>(this SetupConventionFinder<T> fluentMappingsContainer, string referenceSuffix, params IConvention[] otherConventions)
        {
            return fluentMappingsContainer.AddBrownfieldConventions(referenceSuffix, false, otherConventions);
        }

        public static T AddBrownfieldConventions<T>(this SetupConventionFinder<T> fluentMappingsContainer, string referenceSuffix, bool toLowercase, params IConvention[] otherConventions)
        {

            IList<IConvention> brown =
                new IConvention[]
                {
                    Table.Is(x => x.EntityType.Name.ToLowercaseNamingConvention(toLowercase))
                    ,ConventionBuilder.Property.Always(x => x.Column(x.Name.ToLowercaseNamingConvention(toLowercase)))
                    ,ConventionBuilder.Id.Always( x => x.Column(x.Name.ToLowercaseNamingConvention(toLowercase)) )        
                    ,ConventionBuilder.HasMany.Always(x => x.Key.Column( x.NormalizeReference().ToLowercaseNamingConvention(toLowercase) + referenceSuffix )  )
                
                    // Instead of this...
                    // ,ForeignKey.EndsWith(referenceSuffix)                                
                    // ... we do this, so we have direct control on Reference name's casing:
                    ,ConventionBuilder.Reference.Always(x => x.Column( x.Name.ToLowercaseNamingConvention(toLowercase) + referenceSuffix ) )
                
                };

            fluentMappingsContainer.Add(brown.ToArray());

            return fluentMappingsContainer.Add(otherConventions);
        }


        public static string ToLowercaseNamingConvention(this string s)
        {
            return s.ToLowercaseNamingConvention(true);
        }

        public static string ToLowercaseNamingConvention(this string s, bool toLowercase)
        {
            if (toLowercase)
            {
                var r = new Regex(@"
                (?<=[A-Z])(?=[A-Z][a-z]) |
                 (?<=[^A-Z])(?=[A-Z]) |
                 (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace);

                return r.Replace(s, "_").ToLower();
            }
            else
                return s;
        }


        public static string NormalizeReference(this IOneToManyCollectionInstance x)
        {
            

            
            string cannotDeduceReferenceFromValue = "Ambiguous references found. Do explicit column mapping on both end of the objects";
            string cannotDeduceCollectionFromEntity = "Ambiguous collection found. Do explicit column mapping on both end of the objects";

            string parentKeyfield = "";

            bool needExplicitness = false;
            // Find ambiguous in parent
            {
                // string defaultKeyName = x.EntityType.Name + "_id"; // gleaned from FNH's source code


                var parentType = x.EntityType; // e.g. Person

                // Find ClassMapExt of the parentType(e.g. Person)
                var parent = (from r in x.ChildType.Assembly.GetTypes()
                              where r.BaseType.IsGenericType
                                    && r.BaseType.GetGenericTypeDefinition() == typeof(ClassMapExt<>)
                                    && r.BaseType.GetGenericArguments()[0] == parentType
                              select r).Single(); // there is only one class mapping for any objects.

                var parentInstance = Activator.CreateInstance(parent);
                var parentCollectionsOfChildType = 
                            from cr in ((IList<ICollectionMappingProvider>)parent.InvokeMember("ExtCollections", BindingFlags.GetProperty, null, parentInstance, null))
                            where cr.GetCollectionMapping().ChildType == x.ChildType
                            select cr;


                if (parentCollectionsOfChildType.Count() == 1)
                    parentKeyfield = parentCollectionsOfChildType.Single().GetCollectionMapping().Key.Columns.Single().Name;
                else
                {
                    // example: Contacts.  must match one parentCollectionsOfChildType only
                    parentKeyfield = parentCollectionsOfChildType.Where(y => y.GetCollectionMapping().Member.Name == x.Member.Name)
                                    .Single().GetCollectionMapping().Key.Columns.Single().Name;
                }

 
                bool hasAmbigousCollection =
                        parentCollectionsOfChildType.Count() > 1
                        &&
                        parentCollectionsOfChildType.Any(z => !z.GetCollectionMapping().Key.Columns.HasUserDefined());




                if (hasAmbigousCollection)
                    throw new Exception(cannotDeduceCollectionFromEntity);

                needExplicitness = parentCollectionsOfChildType.Any(z => z.GetCollectionMapping().Key.Columns.HasUserDefined());
            }

            

            // Find ambiguous in children
            {

                // Find ClassMapExt of the x.ChildType(e.g. Contact)
                var child = (from r in x.ChildType.Assembly.GetTypes()
                             where r.BaseType.IsGenericType
                                   && r.BaseType.GetGenericTypeDefinition() == typeof(ClassMapExt<>)
                                   && r.BaseType.GetGenericArguments()[0] == x.ChildType  // Contact
                             select r).Single();



                

                var childInstance = Activator.CreateInstance(child); // ContactMapExt                                        

                

                /*
                 * 
                 * References(x => x.Owner)
                 * the Owner's property type is: Person                
                 * can be obtained from:
                 *      cr.GetManyToOneMapping().Member.PropertyType 
                 * 
                 * x.EntityType is: Person
                 * 
                 * */

                var childReferences =
                                    from cr in ((IList<IManyToOneMappingProvider>)child.InvokeMember("ExtReferences", BindingFlags.GetProperty, null, childInstance, null))
                                    where cr.GetManyToOneMapping().Member.PropertyType == x.EntityType
                                    select cr;
                                      

             

                /*
                 if you do in Classmap: References(x => x.Owner).Column("Apple")
                  
                        y.GetManyToOneMapping().Columns.Single().Name == "Apple"
                 
                 if you do in Classmap: References(x => x.Owner)
                  
                        y.GetManyToOneMapping().Columns.Single().Name == "Owner_id"
                 
                 in both cases:
                    
                        y.GetManyToOneMapping().Name == "Owner"
                 */


                //// return string.Join( "$", childReferences.Select(zz => "@" + zz.GetManyToOneMapping().Name + " " + zz.GetManyToOneMapping().Columns.Single().Name + "!" ).ToList().ToArray() );



                if (needExplicitness)
                {
                    // all not defined
                    if (childReferences.All(y => !y.GetManyToOneMapping().Columns.HasUserDefined()))
                    {
                        throw new Exception(
                            string.Format("Explicitness needed on both ends. {0}'s {1} has no corresponding explicit Reference on {2}",
                            x.EntityType.Name, x.Member.Name, x.ChildType.Name));
                    }// all not defined
                    else
                    {
                        var isParentKeyExistingInChildObject = childReferences.Any(z => z.GetManyToOneMapping().Columns.Single().Name == parentKeyfield);

                        if (!isParentKeyExistingInChildObject)
                        {
                            if (childReferences.Count() == 1)
                            {
                                string userDefinedKey = childReferences.Single().GetManyToOneMapping().Columns.Single().Name;
                                throw new Exception(
                                        string.Format(
                                            "Child object {0} doesn't match its key name to parent object {1}'s {2}. Child Key: {3} Parent Key: {4}",
                                            x.ChildType.Name, x.EntityType.Name, x.Member.Name, userDefinedKey, parentKeyfield)
                                            );
                            }
                            else
                            {
                                throw new Exception(
                                        string.Format(
                                            "Child object {0} doesn't match any key to parent object {1}'s {2}. Parent Key: {3}",
                                             x.ChildType.Name, x.EntityType.Name, x.Member.Name, parentKeyfield));
                            }
                        }//if
                        else
                        {
                            return parentKeyfield;
                        }
                    }//if at least one defined
                }// if needExplicitness
                else
                {
                    bool hasUserDefined = childReferences.Count() == 1 && childReferences.Any(y => y.GetManyToOneMapping().Columns.HasUserDefined());

                    if (hasUserDefined)
                    {
                        throw new Exception(
                                string.Format("Child object {0} has explicit Reference while the parent object {1} has none. Do explicit column mapping on both ends",
                                            x.ChildType.Name, x.EntityType.Name));
                    }
                }

                bool hasAmbiguousReference =
                    ( childReferences.Count() > 1 && childReferences.Any(y => !y.GetManyToOneMapping().Columns.HasUserDefined()) )
                    
                    
                    ||
                    
                    
                    ( !needExplicitness && childReferences.Any(y => y.GetManyToOneMapping().Columns.HasUserDefined()) );


                if (hasAmbiguousReference)
                    throw new Exception(cannotDeduceReferenceFromValue);


                return childReferences.Single().GetManyToOneMapping().Name;
            }


            return "";

        }//Normalize


    }// class BrownfieldSystemHelper
}// namespace FluentNHibernate.BrownfieldSystem

No comments:

Post a Comment