Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create Dictionary with LINQ and avoid "item with the same key has already been added" error

Tags:

c#

linq

I want to find a key in a dictionary and replace the value if it is found or add the key/value if it is not.

Code:

public class MyObject
{

    public string UniqueKey { get; set; }
    public string Field1 { get; set; }
    public string Field2 { get; set; }

}

LINQ Solution (throws An item with the same key has already been added.):

Dictionary<string, MyObject> objectDict = csvEntries.ToDictionary(csvEntry => csvEntry.ToMyObject().UniqueKey, csvEntry => csvEntry.ToMyObject());

ForEach solution (works):

Dictionary<string, MyObject> objectDict = new Dictionary<string, MyObject>();
foreach (CSVEntry csvEntry in csvEntries)
{

    MyObject obj = csvEntry.ToMyObject();

    if (objectDict.ContainsKey(obj.UniqueKey))
    {
        objectDict[obj.UniqueKey] = obj;
    }
    else {
        objectDict.Add(obj.UniqueKey, obj);
    }

}

I really liked the LINQ solution but as it stands, it throws the above error. Is there any nice way of avoiding the error and using LINQ?

like image 934
TomSelleck Avatar asked Jul 20 '15 15:07

TomSelleck


2 Answers

You can use GroupBy to create unique keys:

Dictionary<string, MyObject> objectDict = csvEntries
    .Select(csvEntry => csvEntry.ToMyObject())
    .GroupBy(x => x.UniqueKey)
    .ToDictionary(grp => grp.Key, grp => grp.First());

However, instead of grp.First() you could create a collection with ToList or ToArray. On that way you don't take an arbitary object in case of duplicate keys.

Another option is to use a Lookup<TKey, TValue> which allows duplicate keys and even non-existing keys, you get an empty sequence in that case.

var uniqueKeyLookup = csvEntries
    .Select(csvEntry => csvEntry.ToMyObject())
    .ToLookup(x => x.UniqueKey);
IEnumerable<MyObject> objectsFor1234 = uniqueKeyLookup["1234"]; // empty if it doesn't exist
like image 166
Tim Schmelter Avatar answered Oct 15 '22 08:10

Tim Schmelter


Building on Tim's answer, here's an extension method you can use so you don't need to duplicate the implementation throughout your project:

public static class DictionaryExtensions
{
    public static Dictionary<TKey, TValue> ToDictionaryWithDupSelector<TKey, TValue>(
        this IEnumerable<TValue> enumerable,
        Func<TValue, TKey> groupBy, Func<IEnumerable<TValue>, TValue> selector = null) {

        if (selector == null)
            selector = new Func<IEnumerable<TValue>, TValue>(grp => grp.First());

        return enumerable
            .GroupBy(e => groupBy(e))
            .ToDictionary(grp => grp.Key, grp => selector(grp));
    }
}

By default it will choose the first element when there are duplicates, but I have provided an optional parameter where you can specify an alternate selector. Example call to the extension method:

var objList = new List<string[]> {
    new string[2] {"1", "first"},
    new string[2] {"1", "last"},
    new string[2] {"2", "you"},
};
var asDict = objList.ToDictionary(
    arr => arr[0],
    grp => grp.Last()
);
like image 36
Timothy Jannace Avatar answered Oct 15 '22 08:10

Timothy Jannace