Updated on

A lookup in C# is an immutable collection that maps each key to a group of values. We build one with LINQ’s ToLookup() method and read it through the ILookup<TKey, TElement> interface, so one key can hold many values.

When we build our .NET applications, we commonly use dictionaries to store data as key-value pairs. However, dictionaries do not allow us to map multiple values to a single key. The Lookup class fills that gap, so let’s see how it works.

To download the source code for this article, you can visit our GitHub repository.

What Is a Lookup in C#?

In C#, the Lookup<TKey, TElement> class is a dictionary-like data structure that maps keys to a value or collection of values. Microsoft Learn calls what ToLookup() returns “a one-to-many dictionary that maps keys to collections of values”.

The class implements the ILookup<TKey, TElement> interface, which lives in the LINQ namespace, System.Linq. That interface is small: a Count property, an indexer that takes a key, and a Contains() method. It also extends IEnumerable<IGrouping<TKey, TElement>>, so when we loop over a lookup we get one group per key.

Three behaviours set it apart from a dictionary. It is immutable, so nothing can be added or removed once it exists. A duplicate key is not an error, because the value simply joins that key’s group. And a key that is not there gives us an empty sequence instead of a KeyNotFoundException.

We never create one with new. Instead, we call the ToLookup() method on any IEnumerable<T>.

Instances of the Lookup class maintain an internal array of Grouping<TKey, TElement> objects, with each group mapped to a unique key. Each group implements the public IGrouping<TKey, TElement> interface. So, while the lookup is being built, every value we select goes into its key’s group.

The Lookup<TKey, TElement> class and the Dictionary<TKey, TValue> class are similar data structures in that we can use them to map a key to a value. However, there are some differences between these types.

In the next section, let’s discuss these differences and see how they affect our applications.

How Is a Lookup Different From a Dictionary in C#?

Firstly, a Lookup<TKey, TElement> instance is an immutable data structure. This means that once we create a Lookup, we can’t add or remove any item from it. In contrast, a dictionary will allow us to add, delete, or modify its content even after creation.

There are multiple immutable collection types in C# available, if you want to know more, check out our article Immutable Collections in C#.

Secondly, Lookup<TKey, TElement> objects allow us to map a key to a single value or a collection of values. However, dictionaries only allow us to map each of our keys to a single value.

The two also disagree about keys that are not there. A dictionary indexer throws a KeyNotFoundException for a missing key, while a lookup indexer returns an empty sequence. Null keys differ too: ToLookup() groups items with a null key like any other, while ToDictionary() throws an ArgumentNullException.

Finally, lookups do not have a public constructor. We can only create a lookup by invoking the ToLookup() method on a collection that implements the IEnumerable<T> interface. On the other hand, when we work with dictionaries, we have access to a variety of public constructor overloads.

Let’s continue talking about something more hands-on and discuss how to create Lookup objects.

How Do We Create a Lookup With ToLookup()?

As we’ve seen before, we can only create lookups by invoking the ToLookup() method. When we call this method on an enumerable, it performs two actions.

First, it creates a new Lookup instance that has a Grouping<TKey, TElement> array field.

After that, it iterates through all the items in our input enumerable. On each iteration, if the current item’s key is new, it generates a new Grouping<TKey, TElement> instance, maps the group to that key, and then adds the value we selected to the group.

During this process, if we encounter any duplicate key, then it means that we have already created and mapped a group to it. Therefore, we retrieve that group and add our value to it.

Note that the ToLookup() method returns instances of the ILookup interface. This is because it’s always best to return abstract types rather than concrete types when we design APIs.

On .NET 10 there is also a practical reason to follow that advice. When the source is an empty array, ToLookup() returns an internal EmptyLookup<TKey, TElement> that is not a Lookup<TKey, TElement> at all, so code that casts the result to Lookup<TKey, TElement> throws an InvalidCastException. Enumerable.Empty<T>() behaves the same way, while an empty List<T> still returns a castable lookup, so the cast fails only for some callers. We keep the result as ILookup<TKey, TElement> and never cast it.

Now, the input enumerable for our lookup will be a list of students. So, let’s define a Student record:

public record Student(string Name, string Course);

This record will serve as the type for our input lists and we define the string properties Name and Course.

Great! Let’s now proceed to define our Lookup<TKey, TElement> object.

Create a Lookup From a List With a Duplicate

For this, let’s first create our list of students inside a LookupOperations class, which holds the rest of our code:

private static readonly List<Student> _students
    = [
        new("Dan Sorla", "Accounting"),
        new("Dan Sorla", "Economics"),
        new("Luna Delgrino", "Finance"),
        new("Kate Green", "Investment Management")
      ];

Here, we define a list of students where the first student is entered twice but with different courses.

With that, let’s proceed to create our lookup:

public static ILookup<string, string> CreateLookup()
    => _students.ToLookup(s => s.Name, s => s.Course);

We pass in two arguments to the ToLookup() method and invoke it on the _students list.

The first argument, s => s.Name is a function for the keySelector. We use it to specify how our ToLookup() method should extract a key from each item in our input list. In this case, we specify that we want the names of our students to be the keys in the lookup. Then, the second argument, s => s.Course is a function for the elementSelector. We use this selector to specify that we want the courses as values in our lookup.

As we have seen, with these selections, the second appearance of the name, "Dan Sorla", will be a duplicate key in our lookup. However, instead of raising an exception when we try to add this name to our lookup (which is what the LINQ ToDictionary method does with a duplicate key), the ToLookup() method retrieves the initial group mapped to the first entry of the Name which is our key. Then, it will add the new Course string to the group.

When all that is done, we get a Lookup where one key has multiple values mapped to it.

Operations of the Lookup Class

Let’s discuss the various operations we can perform on them. These operations include retrieving items from our lookup, searching our lookup for a key, and retrieving the count of items in our lookup.

As we’ve noted before, lookups are immutable. Therefore, we can’t carry out add, update, and delete operations on our lookup instances.

With that, let’s define an input Lookup instance for these operations:

private static readonly ILookup<string, string> _lookup
    = CreateLookup();

Here, we call the CreateLookup() method we defined in the last section to create the input lookup, having the name of the student as a key, and the courses as the elements.

Let’s divide these operations into different sections and discuss them in depth.

Retrieving Items From a Lookup

Let’s start with retrieving items from our lookups.

First, let’s explore how to get a value from our lookup by specifying its key:

public static IEnumerable<string> RetrieveValuesOfAKeyFromLookup(string key)
    => _lookup[key];

For this, we pass the key of the value we wish to retrieve to the indexer. We get all the values attached to this key as a collection. However, if no value is mapped to this key, we get an empty IEnumerable<string> instance.

You want to know more about indexers? Check out our article Indexers in C#.

Next, let’s discuss how to retrieve all the keys in our lookup:

public static List<string> RetrieveAllKeysFromLookup()
    => _lookup.Select(studentGroup => studentGroup.Key).ToList();

We invoke the Select() method and take the Key of the Lookup element.

Moving on, let’s demonstrate how to retrieve all the values in our lookup:

public static IEnumerable<string> RetrieveAllValuesFromLookup()
    => _lookup.SelectMany(studentGroup => studentGroup);

To retrieve the values in our lookup, we invoke the SelectMany() method to flatten all the Grouping<TKey, TElement> objects in our lookup and yield return all the elements (student’s courses) we added to them.

Searching for a Key in a Lookup

Now, as a next step, let’s explore how to search for a key in our lookup:

public static bool SearchForKeyInLookup(string key)
    => _lookup.Contains(key);

Here, we pass in the key we want to search for to the Contains() method and invoke it. This method returns true if the specified key exists in our lookup. Otherwise, it returns false.

Getting the Count of a Lookup

For the final operation, let’s retrieve the number of items in our lookup:

public static int GetLookupCount()
    => _lookup.Count;

With the Count property, we get the number of keys in our lookup, one per group. Our four students give us 3, because Dan Sorla’s two courses share one key.

Alright! We’ve successfully examined the various operations we can perform on lookups in C#. Let’s now proceed to discuss some situations where we will find them suitable for use in our applications.

When Should We Use a Lookup in C#?

Generally speaking, we can use lookups any time we want to store data as key-value pairs. However, for simple cases, it is advisable to utilize dictionaries. This is because dictionaries are more efficient: we do not define Grouping<TKey, TElement> objects for their keys. When we built both from 10,000 items with unique keys on .NET 10, ToLookup() allocated 1,142,488 bytes and ToDictionary() allocated 283,144, about four times less.

Also, as we saw in one of our examples, lookups can be useful whenever we want to map multiple values to a particular key. 

Additionally, lookups will surely come in handy in cases where we want to easily group our enumerable based on a particular property or set of properties.

Finally, whenever we want to store data in key-value pairs, and we are not sure if all our keys will be unique, it is advisable to utilize a lookup.

How Is ToLookup() Different From GroupBy()?

Both methods group a sequence by a key, and both give us IGrouping<TKey, TElement> objects. The main difference is when the work happens.

ToLookup() runs immediately. It reads the source once, builds every group and keeps them. If the source list changes later, the lookup does not see it.

GroupBy() waits. Microsoft Learn says: “This method is implemented by using deferred execution.” The call only stores the query, and the grouping runs again every time we enumerate the result.

So which one should we pick? If we group once and loop over the groups once, GroupBy() is the natural choice, and it is also the one that runs inside a database query through IQueryable<T>. If we want to read keys many times, or we need a snapshot that stays put when the source changes, ToLookup() fits better, because its indexer goes straight to the key’s group.

Let’s see the difference in code with a method that groups the same list both ways and changes it afterward:

public static (int FromGroupBy, int FromLookup) CountValuesAfterSourceChanges(
    string key, Student newStudent)
{
    List<Student> students = [.. _students];

    var grouped = students.GroupBy(s => s.Name, s => s.Course);
    var lookup = students.ToLookup(s => s.Name, s => s.Course);

    students.Add(newStudent);

    return (grouped.Single(g => g.Key == key).Count(), lookup[key].Count());
}

First, we copy our students into a new list so the shared _students list stays as it is. Then we group the copy both ways, and only after that do we add one more student.

When we call the method with "Kate Green" and new Student("Kate Green", "Taxation"), it returns (2, 1). GroupBy() ran its grouping when we called Single(), after the new student was already in the list, so it counts two courses. The lookup was built before the change, so it still holds one.

Both results are correct, because they answer different questions: “what does the list hold now?” and “what did it hold when we built the lookup?”.

Lookup (ToLookup())Dictionary (ToDictionary())GroupBy()
Values per keyManyOneMany
Duplicate key in the sourceJoins that key's groupThrows ArgumentExceptionJoins that key's group
Key that is not thereIndexer returns an empty sequenceIndexer throws KeyNotFoundExceptionNo indexer, we search the groups
null keyAllowed, grouped like any keyThrows ArgumentNullExceptionAllowed, grouped like any key
Add or remove after buildingNoYesNot a collection, it re-reads the source
When the grouping runsOnce, at the callOnce, at the callEvery time we enumerate it

Conclusion

In this article, we’ve explored the Lookup class in C#.

First, we explained what a Lookup is. Then, we discussed how to create Lookup instances. After that, we examined different ways of retrieving items from our lookups. Subsequently, we explored how to search for a key in our lookups and how to get the count of items in our lookups. Then, we compared ToLookup() with GroupBy() and saw that only the lookup keeps a snapshot of the source.

Before that comparison, we saw four real-world scenarios where lookups are effective tools for handling data in our applications.

Tested with .NET 10 (SDK 10.0.302, runtime 10.0.10) and xUnit 2.9.3.