How to easily merge two anonymous objects with different data structure?

Viewed 394

I would like to merge these two anonymous objects:

var man1 = new {
    name = new {
        first = "viet"
    },
    age = 20
};

var man2 = new {
    name = new {
        last = "vo"
    },
    address = "123 street"
};

Into a single one:

var man = new {
    name = new {
        first = "viet",
        last = "vo"
    },
    age = 20,
    address = "123 street"
};

I looked for a solution but found nothing clever.

2 Answers

Convert the anonymous object to ExpandoObject which is essentially a dictionary of string key and object value:

var man1Expando = man1.ToDynamic();
var man2Expando = man2.ToDynamic();
public static ExpandoObject ToDynamic(this object obj)
{
    IDictionary<string, object> expando = new ExpandoObject();

    foreach (var propertyInfo in obj.GetType().GetProperties())
    {
        var currentValue = propertyInfo.GetValue(obj);
        if (propertyInfo.PropertyType.IsAnonymous())
        {
            expando.Add(propertyInfo.Name, currentValue.ToDynamic());
        }
        else
        {
            expando.Add(propertyInfo.Name, currentValue);
        }
    }

    return expando as ExpandoObject;
}

I'm using a helper extension to establish whether a type is an anonymous one:

public static bool IsAnonymous(this Type type)
{
    return type.DeclaringType is null
        && type.IsGenericType
        && type.IsSealed
        && type.IsClass
        && type.Name.Contains("Anonymous");
}

Then, merge two resulting expando objects into one, but recursively, checking for nested expando objects:

var result = MergeDictionaries(man1Expando, man2Expando, overwriteTarget: true);
public static IDictionary<string, object> MergeDictionaries(
    IDictionary<string, object> targetDictionary,
    IDictionary<string, object> sourceDictionary,
    bool overwriteTarget)
{
    foreach (var pair in sourceDictionary)
    {
        if (!targetDictionary.ContainsKey(pair.Key))
        {
            targetDictionary.Add(pair.Key, sourceDictionary[pair.Key]);
        }
        else
        {
            if (targetDictionary[pair.Key] is IDictionary<string, object> innerTargetDictionary)
            {
                if (pair.Value is IDictionary<string, object> innerSourceDictionary)
                {
                    targetDictionary[pair.Key] = MergeDictionaries(
                        innerTargetDictionary,
                        innerSourceDictionary,
                        overwriteTarget);
                }
                else
                {
                    // What to do when target propety is nested, but source is not?
                    // Who takes precedence? Target nested property or source value?
                    if (overwriteTarget)
                    {
                        // Replace target dictionary with source value.
                        targetDictionary[pair.Key] = pair.Value;
                    }
                }
            }
            else
            {
                if (pair.Value is IDictionary<string, object> innerSourceDictionary)
                {
                    // What to do when target propety is not nested, but source is?
                    // Who takes precedence? Target value or source nested value?
                    if (overwriteTarget)
                    {
                        // Replace target value with source dictionary.
                        targetDictionary[pair.Key] = innerSourceDictionary;
                    }
                }
                else
                {
                    // Both target and source are not nested.
                    // Who takes precedence? Target value or source value?
                    if (overwriteTarget)
                    {
                        // Replace target value with source value.
                        targetDictionary[pair.Key] = pair.Value;
                    }
                }
            }
        }
    }

    return targetDictionary;
}

The overwriteTarget parameter decides which object takes priority when merging.

Usage code:

var man1 = new
{
    name = new
    {
        first = "viet",
    },
    age = 20,
};

var man2 = new
{
    name = new
    {
        last = "vo",
    },
    address = "123 street",
};

var man1Expando = man1.ToDynamic();
var man2Expando = man2.ToDynamic();

dynamic result = MergeDictionaries(man1Expando, man2Expando, overwriteTarget: true);

Console.WriteLine(JsonConvert.SerializeObject(result, Formatting.Indented));

and the result:

{
  "name": {
    "first": "viet",
    "last": "vo"
  },
  "age": 20,
  "address": "123 street"
}

Notice how I assigned the result to dynamic. Leaving compiler assign the type will leave you with expando object presented as IDictionary<string, object>. With a dictionary representation, you cannot access properties in the same manner as if it was an anonymous object:

var result = MergeDictionaries(man1Expando, man2Expando, overwriteTarget: true);

result.name; // ERROR

That's why the dynamic. With dynamic you are losing compile time checking, but have two anonymous objects merged into one. You have to judge for yourself if it suits you.

There's nothing built-in in the C# language to support your use case. Thus, the question in your title needs to be answered with "Sorry, there is no easy way".

I can offer the following alternatives:

  1. Do it manually:

    var man = new {
        name = new {
            first = man1.name.first,
            last = man2.name.first
        },
        age = man1.age,
        address = man2.address
    };
    
  2. Use a class instead of an anonymous type for the resulting type (let's call it CompleteMan). Then, you can

    • create a new instance var man = new CompleteMan(); ,
    • use reflection to collect the properties and values from your "partial men" (man1 and man2),
    • assign those values to the properties of your man.

    It's "easy" in the sense that the implementation will be fairly straight-forward, but it will still be a lot of code, and you need to take your nested types (name) into account.

    If you desperately want to avoid non-anonymous types, you could probably use an empty anonymous target object, but creating this object (var man = new { name = new { first = (string)null, last = (string)null, ...) is not really less work than creating a class in the first place.

  3. Use a dedicated dynamic data structure instead of anonymous C# classes:

Related