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
[
{
"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.