Tripping on Code

Keyed Collection in .Net

January 08, 2021

KeyedCollections are a feature of .Net that I haven't seen used often in the wild. In my experience, dictionaries and hash tables are used in places where KeyedCollections are more appropriate.

A KeyedCollection has features of both dictionaries and collections. It stores data in an ordered structure (like a collection), but also allows item lookup by both indexes and keys. Keyed collections are appropriate when the key is present inside the object.

For instance, in the example below, a username uniquely identifies a person and is the most commonly used property to lookup users, therefore this is a good candidate for a KeyedCollection.

public class Person
{
    public Person(string username, string firstName, string lastName)
    {
        Username = username;
        FirstName = firstName;
        LastName = lastName;
    }

    public string Username { get; }

    public string FirstName { get; }

    public string LastName { get; }
}

Creating a KeyedCollection

The KeyedCollection class is declared in the System.Collections.ObjectModel namespace as an abstract class. To create one, all we have to do is inherit from KeyedCollection<TKey, TItem> and override the GetKeyForItem method.

using System.Collections.ObjectModel;

namespace HelloKeyedCollection.Models
{
    public class People : KeyedCollection<string, Person>
    {
        protected override string GetKeyForItem(Person item)
            => item.Username;
    }
}

Using a KeyedCollection

Let's populate the People class with some data

People people = new People();
people.Add(new Person("mfriedman", "Matthew", "Friedman"));
people.Add(new Person("mgrinch", "Michael", "Grinch"));
people.Add(new Person("agrant", "Alan", "Grant"));
people.Add(new Person("estatler", "Ellie", "Statler"));

Object Lookup

Data in a KeyedCollection can be looked up either by an index or a key. Both lookups have an algorithmic complexity of O(1). An existence check can be performed using the Key as well.


//Lookup by Key
var allan = people["agrant"];

// Lookup by index
var allan = people[2];

// Existence Check
var doesMichaelExist = people.Contains("mgrinch");

JSON Serialization

The KeyedCollection class inherits from Collection, and therefore serializing it into JSON returns a standard list. If the people object was serialized, it would return the following.

[
  {
    "Username": "mfriedman",
    "FirstName": "Matthew",
    "LastName": "Friedman"
  },
  {
    "Username": "mgrinch",
    "FirstName": "Michael",
    "LastName": "Grinch"
  },
  {
    "Username": "agrant",
    "FirstName": "Alan",
    "LastName": "Grant"
  },
  {
    "Username": "estatler",
    "FirstName": "Ellie",
    "LastName": "Statler"
  }
]

This allows services/managers to work with and return a KeyedCollection and an API can return it to the caller without any additional steps.

KeyedCollection without creating derived classes

Creating a derived class every time we wish to use a KeyedCollection might seem like a pain however, it makes sense to create them for types that are going to be reused a lot. In cases where we don't have many instances of it, we can create a simple helper that will allow us to use KeyedCollections without deriving from it every time.

public sealed class SimpleKeyedCollection<TKey, TValue> : KeyedCollection<TKey, TValue>
{
    private readonly Func<TValue, TKey> keySelector;

    public SimpleKeyedCollection(Expression<Func<TValue, TKey>> keySelector)
    {
        this.keySelector = keySelector.Compile();
    }

    protected override TKey GetKeyForItem(TValue item)
    {
        return this.keySelector(item);
    }
}

This class can be used to create KeyedCollections of any type. It is required to pass a key selector to it during construction. This class can be instantiated as follows

var simpleKeyedCollection = new SimpleKeyedCollection<string, Person>(x => x.Username);

simpleKeyedCollection.Add(new Person("mfriedman", "Matthew", "Friedman"));
simpleKeyedCollection.Add(new Person("mgrinch", "Michael", "Grinch"));
simpleKeyedCollection.Add(new Person("agrant", "Alan", "Grant"));
simpleKeyedCollection.Add(new Person("estatler", "Ellie", "Statler"));

Final Thoughts

KeyedCollections should be used a lot more than it is. It is a very powerful collection that offers a lot of features at a very low cost. It does have a little more overhead than either a Collection or a dictionary however, in most cases, the added benefit of using it more than makes up for it.