Back to Blog
C#

Using Lookup in C# LINQ for One-to-Many Maps

c# linq lookup: Learn how to create and use a LINQ Lookup in C# for one-to-many key mappings, and when it beats Dictionary or GroupBy.

LINQC#LookupDictionaryGroupBy
Diagram showing a LINQ Lookup mapping one key to multiple values in C#

c# linq lookup requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

What a Lookup Is in LINQ

A Lookup<TKey, TElement> is a one-to-many key mapping. Where a Dictionary<TKey, TValue> maps a key to a single value, a Lookup maps a key to a sequence of elements. The class implements ILookup<TKey, TElement>, which exposes an indexer and a Contains method but no way to add or remove entries after construction.

You create a Lookup with the ToLookup extension method rather than with a constructor:

var ordersByCustomer = orders.ToLookup(o => o.CustomerId);

Each key in the resulting Lookup corresponds to one or more elements. If a customer has no orders, that customer does not appear as a key at all.

Creating a Lookup with ToLookup

ToLookup executes immediately. When you call it, the entire source sequence is enumerated and the internal hash table is built. This is different from GroupBy, which defers execution until the result is enumerated.

The simplest form takes a key selector:

var byRegion = customers.ToLookup(c => c.Region);

An overload accepts an element selector, so you can project the values stored under each key:

var namesByRegion = customers.ToLookup(c => c.Region, c => c.Name);

A third overload takes a custom IEqualityComparer<TKey>. Use it when keys need case-insensitive comparison or when the default comparer does not fit the type:

var byName = products.ToLookup(p => p.Code, StringComparer.OrdinalIgnoreCase);

Because the source is fully consumed at the call site, a Lookup is a snapshot of the data. If the source is an IEnumerable backed by a database query, the query runs once at this point, not on each access.

Accessing Values by Key

The indexer returns an IEnumerable<TElement> for the requested key. The important behavior is that a missing key returns an empty sequence rather than throwing:

var orders = ordersByCustomer[42]; Console.WriteLine(orders.Count()); // 0 when customer 42 has no orders

This makes the indexer safe to use without a TryGetValue pattern. To check whether a key exists at all, use Contains:

if (ordersByCustomer.Contains(42)) { // key exists }

Iterating over the Lookup yields IGrouping<TKey, TElement> objects, so you can process each key and its values together:

foreach (var group in ordersByCustomer) { Console.WriteLine($"{group.Key}: {group.Count()} orders"); }

Lookup vs Dictionary

The two types serve different purposes. A Dictionary maps a key to a single value and throws KeyNotFoundException when a key is absent. A Lookup maps a key to a collection and returns an empty sequence for a missing key.

BehaviorDictionary<TKey, TValue>Lookup<TKey, TElement>
Duplicate keysNot allowedAllowed
Missing key accessThrowsReturns empty sequence
MutabilityMutableImmutable after creation
CreationConstructor or collection initializerToLookup()

Use a Dictionary when each key must map to exactly one value and you need to add or update entries. Use a Lookup when the source data already contains repeated keys and you only need read access.

Lookup vs GroupBy

GroupBy and ToLookup produce similar groupings, but they differ in execution timing. GroupBy is deferred; the grouping is computed when you enumerate the result. ToLookup is immediate; the grouping is computed when you call it.

This matters when the source sequence is expensive or changes between calls. With GroupBy, each enumeration re-runs the grouping logic. With a Lookup, the data is materialized once and subsequent access is cheap.

A Lookup also supports direct key access through its indexer. GroupBy results do not provide an indexer; you must iterate or use First with a predicate to find a specific group.

Performance and Memory Considerations

Building a Lookup requires one pass over the source and memory for the internal hash table plus the stored elements. The cost is proportional to the number of elements. Once built, key access is O(1) on average, using the same hash-based mechanism as a Dictionary.

The tradeoff is that the Lookup holds references to all elements for the lifetime of the object. If the source is large and you only need a few keys, building a Lookup may waste memory. In that case, filtering the source first or using a Dictionary with lists may be more appropriate.

Repeated access is where a Lookup pays off. If code performs many lookups by the same key, materializing once with ToLookup avoids re-scanning the source for each query.

Limitations and Edge Cases

A Lookup is immutable. There is no Add or Remove method, so you cannot modify it after creation. If you need to update the mapping, build a new Lookup from the changed source or use a Dictionary<TKey, List<TElement>> and manage the lists yourself.

Null keys are not supported. The key selector must not produce null for any element, or the operation throws an ArgumentNullException. This differs from a Dictionary, which also rejects null keys but at the point of insertion.

Thread safety follows the same rule as other collection types: a Lookup is safe for concurrent reads as long as it is not modified, and since it is immutable, concurrent reads are safe after construction.

c# linq lookup: Practical Usage and Code Examples | RYUSLOG DEV