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