C# Alchemy: KeyedCollection for Effortless Data Organization
Tags: csharp, dotnet
Published: 2024-01-17 05:01:50

C# has been declared the [Programming Language of 2023](https://www.techrepublic.com/article/tiobe-index-january-2024/) so it is the perfect time to explore some features of the language and of .Net. In this post which serves to kick-off a series titled _C# Alchemy_, we'll take a look at [`KeyedCollection`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.keyedcollection-2?view=net-8.0), an abstract base class from the Collections framework.  `KeyedCollection` is useful when you need to map a key to value but the key itself is embedded inside the value, for example, a library catalog where the ISBN of each book serves as the key.

### The Basics

To illustrate the usefulness of `KeyedCollection`, let's build a Pokédex—one of the most important advancements in modern technology! First we define our Pokemon class:

```csharp
public class Pokemon {
    
    public Pokemon(string name, int hp, string type) {
        Name=name;
        Type=type;
        HP=hp;
    }

    public readonly string Name;

    public readonly int HP;

    public readonly string Type;

}
```

Since each species of Pokémon has a unique name, we'll use it as the key for our Pokédex.  To create the Pokédex, we will extend `KeyedCollection`.  Notice from the class definition of `KeyedCollection` that it has two type parameters, one for the data type of the key and one for the data type of the data itself:

```csharp
public abstract class KeyedCollection<TKey,TItem> : 
  System.Collections.ObjectModel.Collection<TItem>
```

Assuming you are familiar with using `Dictionary` for key to value mappings, the immediate benefit of `KeyedCollection` when the key is a member of the value should be clear. To define the Pokédex, extend `KeyedCollection` using `string` and `Pokemon` as the type parameters:

```csharp
using System.Collections.ObjectModel;

public class Pokedex : KeyedCollection<string,Pokemon> {

    override protected string GetKeyForItem(Pokemon pokemon) {
        return pokemon.Name;
    }
}
```

As I am neither a Pokémon Master nor Trainer, I deferred to my kids for a starter pack of some Pokémon to add to our Pokédex:

```csharp
List<Pokemon> myPokemon = new() {
    new Pokemon("Eevee", 55, "Normal"),
    new Pokemon("Pikachu", 35, "Electric"),
    new Pokemon("Vaporeon", 130, "Water"),
    new Pokemon("Lilligant", 70, "Grass"),
    new Pokemon("Floragato", 61, "Grass")
};

Pokedex pokedex = new();
myPokemon.ForEach(
    pokemon => pokedex.Add(pokemon));

Console.Out.WriteLine(pokedex.Count());
```
```bash
5
```

Lets add more:
```csharp
pokedex.Add(new Pokemon("Sandaconda", 72, "Ground"));
pokedex.Add(new Pokemon("Eevee", 55 , "Normal"));
```

Since Eevee is already in the Pokédex and `KeyedCollection` follows the conventions for a Collection, an exception is thrown when trying to add a Pokémon that is already in the Pokédex:

```bash
Error: System.ArgumentException: An item with the same key has already been added. Key: Eevee

```
Use the `Contains` method to check if the Pokémon is recorded in the Pokédex:
```csharp
bool havePikachu = pokedex.Contains("Pikachu");
Console.Out.WriteLine(havePikachu);
```

```bash
True
```

Remove a Pokémon from the Pokédex with `Remove`:

```csharp
pokedex.Remove("Pikachu");
bool havePikachu = pokedex.Contains("Pikachu");
Console.Out.WriteLine(havePikachu);
```

```bash
False
```
Pokémon can be accessed by name.  Lets add an extension method to the `Pokemon` class for printing it and demonstrate retrieval.  You may also iterate over every Pokémon in the Pokédex:

```csharp
public static void Print(this Pokemon pokemon) {
    Console.Out.WriteLine($"{pokemon.Name}");
    Console.Out.WriteLine($"HP: {pokemon.HP}");
    Console.Out.WriteLine($"{pokemon.Type} Type");
}

Pokemon eevee = pokedex["Eevee"];
eevee.Print();
```

```bash
Eevee
HP: 55
Normal Type
```
```csharp
foreach(Pokemon pokemon in pokedex) {
    Console.Out.WriteLine("---");
    pokemon.Print();
}
```

```bash
---
Eevee
HP: 55
Normal Type
---
Vaporeon
HP: 130
Water Type
---
Lilligant
HP: 70
Grass Type
---
Floragato
HP: 61
Grass Type
---
Sandaconda
HP: 72
Ground Type
```

LINQ operations may also be used on the Pokédex to easily filter, query, and transform data:

Count the number of Pokémon with HP larger than 70:
``` csharp
pokedex.Where(pokemon=>pokemon.HP > 70).
    Count()
```

```bash
2
```

Return the value of the highest HP in the Pokédex:

```csharp
pokedex.Max(pokemon=>pokemon.HP)
```

```bash
130
```
### Using Generics

The type parameters may also take advantage of generics to extend capabilities.  Here, we'll create an interface to encapsulate our Pokémon and use it to specialize based on the type of the Pokémon species.  The Pokédex will still use the name of a Pokémon as its key, but the data type of the Pokémon entry is deferred until the Pokédex is created.  We'll constrain the type parameter of the Pokémon using the interface type:

```csharp
public interface IPokemon {

    string Name {get;}

    int HP {get;}

    string Type {get;}

}

public class WaterPokemon : IPokemon {

    private readonly string _name;
    private readonly int _hp;

    public WaterPokemon(string name, int hp) {
        _name=name;
        _hp = hp;
    }

    public string Name => _name;

    public int HP => _hp;

    public string Type => "Water";

}

public class GrassPokemon : IPokemon {

    private readonly string _name;
    private readonly int _hp;

    public GrassPokemon(string name, int hp) {
        _name=name;
        _hp = hp;
    }

    public string Name => _name;

    public int HP => _hp;

    public string Type => "Grass";

}
```

```csharp
public class Pokedex<TPokemon> : KeyedCollection<string,TPokemon>
 where TPokemon : IPokemon {

    override protected string GetKeyForItem(TPokemon pokemon) {
        return pokemon.Name;
    }  
 }
```

```csharp
    WaterPokemon vaporeon = new WaterPokemon("Vaporeon", 130);
    GrassPokemon lilligant = new GrassPokemon("Lilligant", 70);
    GrassPokemon florogato = new GrassPokemon("Floragato", 61);

    Pokedex<WaterPokemon> waterPokemon = new();
    Pokedex<GrassPokemon> grassPokemon = new();
    Pokedex<IPokemon> allPokemon = new();

    waterPokemon.Add(vaporeon);

    grassPokemon.Add(lilligant);
    grassPokemon.Add(florogato);

    allPokemon.Add(vaporeon);
    allPokemon.Add(lilligant);
    allPokemon.Add(florogato);
```

### Final Thoughts and Caveats
`KeyedCollection` provides a simple, expressive way to do a key to value mapping when the key is a member of the value itself. Operations on the collection are constant time and uniqueness is guaranteed and enforced by the API.  The unique key is a natural identifier for the data, allowing for cleaner, more readable and intuitive code. It doesn't come with any major drawbacks but if you were paying attention to the code then you might have noticed the use of the `readonly` modifier. `KeyedCollection` does come with one big caveat - immutability. When using `KeyedCollection` you want to make sure that your keys are immutable.  

Now go make some `KeyedCollection` magic.  You can find a Notebook with the code [here](https://gist.github.com/mjamesharmon/4c19ae0e19fb660ae5c08414b4d57850).