Created
August 3, 2012 16:17
-
-
Save jpoehls/3249136 to your computer and use it in GitHub Desktop.
SmartGroupingResultTransformer for NHibernate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// SQL results like this: | |
/// StateId Name Cities_CityId Cities_Name Counties_CountyId Counties_Name | |
/// | |
/// Would be grouped into a distinct list of States. | |
/// If the State object had collection properties for Cities and Counties | |
/// then they would be populated appropriately. | |
/// | |
/// Only supports a single level of collection. That is you can't have Cities_Residents_Name returned | |
/// and expect it to populate a State.Cities.Residents collection. Instead it would be ignored, | |
/// or used to set a State.Cities.Residents_Name property if that existed. | |
/// | |
/// This will throw exceptions if a SQL column is returned that cannot be mapped. | |
/// | |
/// Grouping is done based on the Equals() method of the result class. | |
/// | |
/// This class is thread safe. It is recommended to keep a static instance around for each QUERY it is used for. | |
/// Do not share an instance of this between queries! | |
/// </summary> | |
public class SmartGroupingResultTransformer : IResultTransformer | |
{ | |
private const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; | |
private readonly System.Type resultClass; | |
private readonly ConstructorInfo constructor; | |
private readonly PropertyInfo[] resultClassCollectionProperties; | |
private Dictionary<string, AliasProperty> aliasProperties = null; | |
private object syncRoot = new Object(); | |
private class AliasProperty | |
{ | |
/// <summary> | |
/// The <see cref="PropertyInfo"/> for the result class's | |
/// collection property that the alias property maps to. | |
/// Null if the alias property is not on a child collection. | |
/// </summary> | |
public PropertyInfo CollectionPropertyInfo; | |
/// <summary> | |
/// The <see cref="Type"/> of item in the collection property. | |
/// </summary> | |
public Type CollectionItemType; | |
/// <summary> | |
/// The <see cref="PropertyInfo"/> that the alias maps to. | |
/// Might be a property of a child collection item. | |
/// </summary> | |
public PropertyInfo PropertyInfo; | |
} | |
public SmartGroupingResultTransformer(System.Type resultClass) | |
{ | |
if (resultClass == null) | |
{ | |
throw new ArgumentNullException("resultClass"); | |
} | |
this.resultClass = resultClass; | |
constructor = resultClass.GetConstructor(flags, null, System.Type.EmptyTypes, null); | |
// if resultClass is a ValueType (struct), GetConstructor will return null... | |
// in that case, we'll use Activator.CreateInstance instead of the ConstructorInfo to create instances | |
if (constructor == null && resultClass.IsClass) | |
{ | |
throw new ArgumentException("The target class of a AliasToBeanResultTransformer need a parameter-less constructor", | |
"resultClass"); | |
} | |
// Find all of the collection properties that we support filling. | |
resultClassCollectionProperties = resultClass.GetProperties(flags) | |
.Where(prop => typeof(IList).IsAssignableFrom(prop.PropertyType) | |
&& prop.PropertyType.IsGenericType | |
&& prop.PropertyType.GenericTypeArguments.Length == 1) | |
.ToArray(); | |
} | |
private void PopulateAliasPropertyCache(string[] aliases) | |
{ | |
lock (syncRoot) | |
{ | |
if (aliasProperties != null) | |
return; | |
aliasProperties = new Dictionary<string, AliasProperty>(aliases.Length); | |
for (int i = 0; i < aliases.Length; i++) | |
{ | |
var alias = aliases[i]; | |
// First try to get a property of the result class with this name. | |
var prop = resultClass.GetProperty(alias, flags); | |
if (prop == null && alias.Contains('_')) | |
{ | |
var aliasSplit = alias.Split(new[] { '_' }); | |
var resultClassAlias = aliasSplit[0]; | |
var collectionClassAlias = aliasSplit.Length >= 2 ? aliasSplit[1] : null; | |
// Check if there is a collection property with this name. | |
if (collectionClassAlias != null) | |
{ | |
var collectionProp = resultClassCollectionProperties.SingleOrDefault(x => x.Name == resultClassAlias); | |
if (collectionProp != null) | |
{ | |
// Check if the collection item type has a matching property. | |
prop = collectionProp.PropertyType.GenericTypeArguments[0].GetProperty(collectionClassAlias, flags); | |
if (prop != null) | |
{ | |
aliasProperties.Add(alias, new AliasProperty() | |
{ | |
CollectionPropertyInfo = collectionProp, | |
CollectionItemType = collectionProp.PropertyType.GenericTypeArguments[0], | |
PropertyInfo = prop | |
}); | |
} | |
} | |
} | |
} | |
else | |
{ | |
// Alias maps to a property of the result class. | |
aliasProperties.Add(alias, new AliasProperty() | |
{ | |
PropertyInfo = prop | |
}); | |
} | |
if (prop == null) | |
{ | |
throw new ApplicationException("Invalid alias '" + alias + "'. Could not map to a property on the " + resultClass.Name + " type."); | |
} | |
} | |
} | |
} | |
public object TransformTuple(object[] tuple, String[] aliases) | |
{ | |
if (aliases == null) | |
{ | |
throw new ArgumentNullException("aliases"); | |
} | |
object result; | |
try | |
{ | |
if (aliasProperties == null) | |
PopulateAliasPropertyCache(aliases); | |
// if resultClass is not a class but a value type, we need to use Activator.CreateInstance | |
result = resultClass.IsClass | |
? constructor.Invoke(null) | |
: NHibernate.Cfg.Environment.BytecodeProvider.ObjectsFactory.CreateInstance(resultClass, true); | |
for (int i = 0; i < aliases.Length; i++) | |
{ | |
var alias = aliases[i]; | |
var value = tuple[i]; | |
var aliasProperty = aliasProperties[alias]; | |
if (aliasProperty.CollectionPropertyInfo == null) | |
{ | |
// Set the property on the result class | |
aliasProperty.PropertyInfo.SetValue(result, value); | |
} | |
else | |
{ | |
// Property is on a child collection item | |
IList list = aliasProperty.CollectionPropertyInfo.GetValue(result) as IList; | |
if (list == null) | |
{ | |
// Instantiate the collection... | |
list = (IList)Activator.CreateInstance(aliasProperty.CollectionPropertyInfo.PropertyType); | |
aliasProperty.CollectionPropertyInfo.SetValue(result, list); | |
} | |
// Get the first item in the collection (if there is one) | |
// The way this works is we will only ever add 1 item to the collection. | |
// TransformList will group the parent objects and the collections will be merged. | |
object listItem = null; | |
if (list.Count > 0) | |
{ | |
listItem = list[0]; | |
} | |
if (listItem == null) | |
{ | |
// Instantiate a new collection item. | |
listItem = Activator.CreateInstance(aliasProperty.CollectionItemType); | |
list.Add(listItem); | |
} | |
// Set the property on this item | |
aliasProperty.PropertyInfo.SetValue(listItem, value); | |
} | |
} | |
} | |
catch (InstantiationException e) | |
{ | |
throw new HibernateException("Could not instantiate result class: " + resultClass.FullName, e); | |
} | |
catch (MethodAccessException e) | |
{ | |
throw new HibernateException("Could not instantiate result class: " + resultClass.FullName, e); | |
} | |
return result; | |
} | |
public IList TransformList(IList collection) | |
{ | |
// Get the first item in the collection. | |
// We will assume that all items in the collection are of the same type. | |
object firstItem = (collection.Count > 0) ? collection[0] : null; | |
var itemType = firstItem.GetType(); | |
// Track which items we want to remove. | |
var removeList = new List<int>(); | |
// Group the items based on the ID property's value. | |
for (var i = 0; i < collection.Count; i++) | |
{ | |
var thisItem = collection[i]; | |
var mergeItemIndex = collection.IndexOf(thisItem); | |
if (mergeItemIndex != i) | |
{ | |
var mergeItem = collection[mergeItemIndex]; | |
// Merge the collection properties of 'this' object with the 'merge' item | |
foreach (var prop in itemType.GetProperties(flags)) | |
{ | |
if (typeof(IList).IsAssignableFrom(prop.PropertyType)) | |
{ | |
var mergeFrom = (IList)prop.GetValue(thisItem); | |
var mergeTo = (IList)prop.GetValue(mergeItem); | |
if (mergeFrom != null && mergeTo != null) | |
{ | |
foreach (var item in mergeFrom) | |
{ | |
if (!mergeTo.Contains(item)) | |
{ | |
mergeTo.Add(item); | |
} | |
} | |
} | |
} | |
} | |
// add 'this' item to the removal list | |
// insert it at the beginning of the list | |
// so that when we remove the items later they | |
// will be removed from the end of the list first. | |
removeList.Insert(0, i); | |
} | |
} | |
// remove some items | |
foreach (var i in removeList) | |
{ | |
collection.RemoveAt(i); | |
} | |
return collection; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment