Skip to content

Instantly share code, notes, and snippets.

@jpoehls
Created August 3, 2012 16:17
Show Gist options
  • Save jpoehls/3249136 to your computer and use it in GitHub Desktop.
Save jpoehls/3249136 to your computer and use it in GitHub Desktop.
SmartGroupingResultTransformer for NHibernate
/// <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