Tranquillity in C# with EasyDictionary

Dictionary<TKey, TValue> is probably among the most used collections in C#. If you are reading this blog then I am assuming you know the fundamentals of this data structure and if you are not, you can have a look at The .NET Dictionary which covers it in detail. As a summary however:

  • The Dictionary<TKey, TValue> is a Hash table mapping a set of keys to a set of values.
  • Each key in the dictionary must be unique according to the IEqualityComparer<TKey> (used to determine the equality of the keys in the dictionary).
  • Retrieving a value by using its key has a complexity of O(1) (very fast).

Typical usecase

Most of the time, I use a dictionary to map a model to one of its properties; Let's say an Id or Name. For example, given the model:

public sealed class Person  
{
    public Guid Id { get; }
    public string Name { get; }
    public uint Age { get; }

    public Person(Guid id, string name, uint age)
    {
        Name = name;
        Age = age;
        Id = id;
    }
}

And given a collection of people from somewhere:

var people = Enumerable.Range(1, 10)  
    .Select(n => new Person(Guid.NewGuid(), "P-" + n.ToString(), (uint)n));

Which we now need to make available for fast lookups based on the Id of a given person.

Well that's easy, let's new-up a Dictionary and add our people to it:

var dic = new Dictionary<Guid, Person>();  
foreach(var p in people)  
{
    dic[p.Id] = p;
    // or if you don't like the index syntax then you can instead do: 
    // dic.Add(p.Id, p);
}

We can also create a dictionary directly from the IEnumerable<Person> by doing:

var dic = Enumerable.Range(1, 10)  
    .Select(n => new Person(Guid.NewGuid(), "P-" + n.ToString(), (uint)n))
    .ToDictionary(p => p.Id, p => p);

Okay not the most verbose code to complain about but if I take into account that every time I want to Add, TryGet or check for existence (Contains) of a given value I need to specify the TKey explicitly then I would be able to hear my OCD screaming at me to make things simpler.

What if...

Would it not be more elegant if we could just add a person and let the dictionary get the Id from it? Something similar to:

var dic = new EasyDictionary<Guid, Person>(...);  
foreach(var p in people)  
{
    dic.Add(p)
}

In order to do this, we would need some sort of a key selector which for a given TValue would return a TKey. In other words, we need a Func<TValue, TKey> which in our example would be a Func<Person, Guid>.

Now that we know what a key selector would look like, we can start thinking about the structure of our new abstraction:

public sealed class EasyDictionary<TKey, TValue>  
{
    public EasyDictionary(Func<TValue, TKey> keySelector)
    {
        KeySelector = keySelector;
    }

    public Func<TValue, TKey> KeySelector { get; }
    ...
}

All we have to do now is put a Dictionary<TKey, TValue> inside our wrapper, implement IReadOnlyDictionary<TKey, TValue> as well as ICollection<TValue> and then redirect all the required methods and properties to our internal dictionary.

Job done!

I am going to spare you the unexciting code here and instead, point you to the completed version in EasyDictionary available as part of the Easy.Common NuGet package.

Armed with our much smarter dictionary, let us see what we can do with it (look at the unit tests for more examples):

var people = Enumerable.Range(1, 10)  
    .Select(n => new Person(Guid.NewGuid(), "P-" + n.ToString(), (uint)n))
    .ToArray();

var easyDic = new EasyDictionary<Guid, Person>(p => p.Id);  
foreach(var p in people)  
{
    easyDic.Add(p);
}

// Let's lookup an existing value.
var firstPerson = people[0];

easyDic.Contains(firstPerson);                          // true  
easyDic.ContainsKey(firstPerson.Id);                    // true

easyDic[firstPerson.Id];                                // firstPerson

easyDic.TryGetValue(firstPerson.Id, out Person yep);    // true (yep is firstPerson)

easyDic.Remove(firstPerson.Id);                         // true (removed)  
easyDic.Remove(firstPerson);                            // false (no longer there)

easyDic.TryGetValue(firstPerson.Id, out Person _);      // false (no longer there)  

We can also enumerate over the items:

foreach (Person p in easyDic) { /* do what you want with p */ }  

But what about over-writing an existing item? Well, we can use AddOrReplace for that:

var olderPerson = new Person(  
    firstPerson.Id,
    firstPerson.Name,
    firstPerson.Age + 41);

easyDic.AddOrReplace(olderPerson); // true;  

Bonus point

We can even throw a bunch of extension methods to be able to do:

var dic = Enumerable.Range(1, 10)  
    .Select(n => new Person(Guid.NewGuid(), "P-" + n.ToString(), (uint)n))
    .ToEasyDictionary(p => p.Id);


Now that is tranquillity!

Nima Ara

@NimaAra