1

I have some rather large files around that I would like to load in automatically with deserialization and then simply merge together, if possible maintaining memory references in a central object to make the merge as benign as possible.

Merges seem to be anything but simple though, in concept it seems easy

  • If there are some nulls, use the non null values
  • If there are if there are conflicting objects, dig deeper and merge their internals or just prefer one, maybe write custom code just for those classes to merge.
  • If there are collections, definitely combine them, if there are keys, like in a dictionary, try to add them, when there is a key conflict, as before, merge them or prefer one.

I've seen a lot of people around stack recommending I use Automapper to try and achieve this, though that seems flawed. Automapper isn't made for this and the overall task doesn't seem complex enough to warrant it. Its also not amazing encapsulation to put all your class specific merge code anywhere but that class. Your data pertaining to a given aspect of your code should sit in central locations, like a class object, to enable programmers to understand the usage of the data structure around them more readily. So I don't feel that automapper is a good solution for merging objects rather than simply, keeping one.

How would you recommend automating the merge of two structurally identical c# objects with nested heirarchies of custom classes?

I will post my own solution as well, but I encourage other developers, certainly many more intelligent than I, to recommend solutions.

2 Answers 2

1

While @JodySowald's answer decribes a nice generic approach, merging sounds to me like something that could involve an awful lot of class-specific business logic.

I would simply add a MergeWith method to each and every class in my hierarchy, down to a level where "merging" means a simple repeatable generic operation.

class A
{
    string Description;
    B MyB {get; set;}

    public void MergeWith(A other)
    {
         // Simple local logic
         Description = string.IsNullOrWithSpace(Description) ? other.Description : Description;
         // Let class B do its own logic
         MyB = MyB.MergeWith(other.MyB);
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

I agree that it would be nice to have merge functionality in every class, but it isn't as scalable, its both functionality that developers would need to remember to implement every time a new structure is implemented and its cumbersome to specifically list the merge functionality of every property in every class. this is the goal of reflection and a generic recursion. and the purpose of an ICombinable interface is to allow exactly this functionality you've referred to only where needed.
I guess it depends on the use case. If it has to be scalable from the start, your solution is certainly preferable. On the other hand, for a straight-forward, one-time implementation (like connecting to a legacy-application that isn't maintained anymore), I would prefer mine because it has less conceptual overhead. If ever necessary,m switching from my idea to yours is always possible with hardly more than a bunch of copy/paste actions, so initially I'd tend towards YAGNI (though I don't know the details of OP's use case)
That's fair oerkelens, My intended use case is not migration. For that please use a tool like Automapper, its great at that. Mine is for symmetrical data structures in which most of the data is configuration and dictionaries and therefore has a preference towards non null data and rare conflicts
1

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);
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.