I think that in ~70% of use cases, someone will have a large hierarchical structure of many classes in a class library and will wish to be able to merge the whole hierarchy at once. For that purpose I think the code should iterate across the properties of the object and nested properties of subclasses, but only ones defined in the assembly you've created. No merging the internals of System.String, who knows what could happen. So only internal types to this assembly should be dug into for further merging
var internalTypes = Assembly.GetExecutingAssembly().DefinedTypes;
We also need a way to define custom code on a given class, there are always edge cases. I believe that this is what interfaces were created for, to generically define functionality for several classes and have a specific implementation available to the specific class. But I found that if merging requires knowledge of the data hierarchically above this class, such as the key it is stored with in a dictionary or perhaps an enum indicating the types or modes of data present, a reference to the containing datastructure should be available. So I defined a quick interface, ICombinable
internal interface ICombinable
{
/// <summary>
/// use values on the incomingObj to set correct values on the current object
/// note that merging comes after the individual DO has been loaded and populated as necessary and is the last step in adding the objects to the central DO that already exists.
/// </summary>
/// <param name="containingDO"></param>
/// <param name="incomingObj">an object from the class being merged in.</param>
/// <returns></returns>
ICombinable Merge(object containingDO, ICombinable incomingObj);
}
Bringing this together into a functional piece of code basically requires a little bit of property reflection, a little bit of recursion, and a little bit of logic, all of which is nuanced, so I just commented my code isntead of explaining it before hand. Since the goal is to affect a central object and not to create a new, merged copy, this is an instance method in the base class of the datastructure. but you could probably convert it to a helper method pretty easily.
internal void MergeIn(Object singleDO)
{
var internalTypes = Assembly.GetExecutingAssembly().DefinedTypes;
var mergableTypes = internalTypes.Where(c => c.GetInterfaces().Contains(typeof(ICombinable)));
MergeIn(this, this, singleDO, internalTypes, mergableTypes);
}
private void MergeIn(Object centralDORef, object centralObj, object singleObj, IEnumerable<TypeInfo> internalTypes, IEnumerable<TypeInfo> mergableTypes)
{
var itemsToMerge = new List<MergeMe>();
//all at once to open up parallelization later.
IterateOver(centralObj, singleObj, (f, t, i) => itemsToMerge.Add(new MergeMe(t, f, i)));
//check each property on these structures.
foreach (var merge in itemsToMerge)
{
//if either is null take non-null
if (merge.From == null || merge.To == null)
merge.Info.SetValue(centralObj, merge.To ?? merge.From);
//if collection merge
else if (merge.Info.PropertyType.IsGenericType && merge.Info.PropertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(IList<>)))
foreach (var val in (IList)merge.From)
((IList)merge.To).Add(val);
//if dictionary merge
else if (merge.Info.PropertyType.IsGenericType && merge.Info.PropertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(IDictionary<,>)))
{
var f = ((IDictionary)merge.From);
var t = ((IDictionary)merge.To);
foreach (var key in f.Keys)
if (t.Contains(key))
{
//if conflicted, check for icombinable
if (merge.Info.PropertyType.GenericTypeArguments[1].IsAssignableFrom(typeof(ICombinable)))
t[key] = ((ICombinable)t[key]).Merge(centralDORef, (ICombinable)f[key]);
}
else
t.Add(key, f[key]);
}
//if both non null and not collections, merge.
else if (merge.From != null && merge.To != null)
{
//check for Icombinable.
if (merge.Info.PropertyType.IsAssignableFrom(typeof(ICombinable)))
merge.Info.SetValue(centralObj, ((ICombinable)merge.To).Merge(centralDORef, (ICombinable)merge.From));
//if we made the object, dig deeper
else if (internalTypes.Contains(merge.Info.PropertyType))
{
//recurse.
MergeIn(centralDORef, merge.To, merge.From, internalTypes, mergableTypes);
}
//else do nothing, keeping the original
}
}
}
private class MergeMe{
public MergeMe(object from, object to, PropertyInfo info)
{
From = from;
To = to;
Info = info;
}
public object From;
public object To;
public PropertyInfo Info;
}
private static void IterateOver<T>(T destination, T other, Action<object, object, PropertyInfo> onEachProperty)
{
foreach (var prop in destination.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
onEachProperty(prop.GetValue(destination), prop.GetValue(other), prop);
}