Domanda

I have multiple dynamic objects that (most of the time) differ in only a few values. I want to be able to merge these objects into one single object and in case of a conflict (two values aren't the same) then I want these to be stored in a collection or another dynamic object (if possible).

I'm usisng the expandoObject class so I can cast my objects into a dictionairy and try to merge that but I haven't found any articles or sources on merging dictionairies that create collections with conflicts.

Is there a way to do this in an easy to implement and efficient fashion?

I'll post some code samples to give you a small idea of what I'm trying to accomplish

//returns a dynamic object from JSON (output from DataBase)
JsonReader reader = new JsonReader();
dynamic object = reader.Read(input);

//Creates a dictionairy with all the fieldnames and values.
IDictionary<string, dynamic> properties = (IDictionary<string, dynamic>)object;

//My goal is to merge multiple of these objects into one single object and
//creating collections when values are different from each other.
È stato utile?

Soluzione

Your problem seems to be based on the IDictionary<> interface. It assumes there is only one value for each key. I believe that you want rather to move to a LINQ-ish IEnumerable<IGrouping<TKey,TValue>> that represents a list of keyed value collections.

LINQ emits such objects when you do a .GroupBy or .ToLookup calls.
Let's play then:

using System.Linq;

Dictionary<string, dynamic> A = ...;
Dictionary<string, dynamic> B = ...;

// naiive atempt:
var lookup = 
    A.Keys.Concat(B.Keys)
        .ToDictionary(key => key, key => new dynamic[]{ A[key], B[key] } ));

Of course it will not work, but I've written it to see what problems would it expose. First - you will probably get duplicate keys when Concat'ing. Then not all keys are in A and B, so the indexers woudl throw. Next, I happened to assume that the original objects were string-to-one-value, and you are likely to work on already-collided objectts. Then, this uses only 2 A/B objects, while you may want to work on multiple..

IEnumerable<Dictionary<string, dynamic>> inputs = ....;

// btw. GropuBy returns a lookup, too :) a key -> all matches
var matched_by_keys = inputs.GroupBy(pair => pair.Key);

var final = matched_by_keys.ToDictionary(group => group.Key, group => group);

Look there. I've taken all dictionaries and "just paired them up" according to their Keys. As a result, I've got a lookup that binds each existing Key to a series of Values that were earlier assigned to that key. The resulting matched_by_keys is an Enumerable, not a dictionary, so it was later translated to it. Look at the parameters to ToDictionary: the Group is itself a IEnumerable just a key had to be pointed out and the group is unchanged.

Still, the input works only on IDictionary that is single-valued. If you need to do such things with multi-valued inputs too, you can easily transalte IDictionaries to lookups and perform the operations on them:

IEnumerable<Dictionary<string, dynamic>> inputs1 = ....; // normal items
IEnumerable<ILookup<string, dynamic>> inputs2 = ....; // items that previously already collided

var allAsLookups =
    inputs1.ToLookup(pair => pair.Key, pair => pair.Value)
    .Concat( inputs2 );

// btw. GropuBy returns a lookup, too :) a key -> all matches
var matched_by_keys = lookups.GroupBy(lk => lk.Key);

var final1 = matched_by_keys.ToDictionary(group => group.Key, group => group.SelectMany(s=>s));

Note how the final groups had to be now translated. In this example, groupsis not IEnumerable<Value>, but IEnumerable<IEnumerable<Value>> becuase the inputs were allowed to be multi-valued, even if they had only 1 value. So, thit had to be flattened, and this is done by SelectMany. That in turn didnt need to project anything, as the item was already IEnumerable, so a s=>s was enough.

Using different overloads of GroupBy, ToLookup, and ToDictionary you may achieve many useful effects. Play with the overloads!

Altri suggerimenti

Since the value is a dynamic you can indeed store either a value or a collection, as you see fit.

We need some kind of collection of dictionaries, in this case an array.

var props1 = new Dictionary<string, dynamic> 
    {
        {"uniq1", "uniq1"},
        {"same", "same1"},
    };
var props2 = new Dictionary<string, dynamic> 
    {
        {"uniq2", "uniq2"},
        {"same", "same2"},
    };
var props3 = new Dictionary<string, dynamic> 
    {
        {"uniq3", "uniq3"},
        {"same", "same3"},
    };

var props = new[] { props1, props2, props3 };

Then get the first one and merge that with the following. If a key already exist then check if it's already a List or if we have to create one. If a key does not exist then simply add it.

var merged = props.First();

foreach (var prop in props.Skip(1))
{
    foreach (var key in prop.Keys)
    {
        dynamic oldValue;
        if (merged.TryGetValue(key, out oldValue))
        {
            if (oldValue is IList<dynamic>)
                merged[key].Add(prop[key]);
            else
                merged[key] = new List<dynamic> { oldValue, prop[key] };
        }
        else
            merged.Add(key, prop[key]);
    }
}

merged will be set to:

merged
Count = 4
    [0]: {[uniq1, uniq1]}
    [1]: {[same, System.Collections.Generic.List`1[System.Object]]}
    [2]: {[uniq2, uniq2]}
    [3]: {[uniq3, uniq3]}
merged["same"]
{System.Collections.Generic.List<object>}
    [0]: "same1"
    [1]: "same2"
    [2]: "same3"

If your dynamic object is Expando, you can use Linq as below:

var objectList = new List<dynamic>() {...};

var result = objectList.SelectMany(obj => 
                           (IDictionary<string, dynamic>)obj.ToList())
            .GroupBy(pair => pair.Key)
            .ToDictionary(group => group.Key,
                          group => group.Select(pair => pair.Value));

First, use SelectMany to select all KeyValuePair<string, dynamic> from dynamic objects, this way will accept the list has the same key.

Then, group by Key and use function ToDictionary to get your result.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top