Guide to LINQ ToLookup Collections in C

In C# LINQ, the ToLookup extension method creates an ILookup<TKey, TElement> collection, which functions as an immutable, one-to-many dictionary (often called a multimap). This article provides a comprehensive overview of lookup collections, explaining how ToLookup works, its core characteristics, how it compares to Dictionary<TKey, TValue> and GroupBy, and practical examples of how to use it in your code.

What Is ILookup<TKey, TElement>?

An ILookup<TKey, TElement> is a specialized collection that maps a single key to an IEnumerable<TElement> of values. Unlike standard key-value pairs where each key must map to a single value, a lookup is designed specifically to group multiple values under a single key.

When you call ToLookup on an IEnumerable<T>, LINQ immediately iterates through the source sequence, evaluates all elements, and organizes them into an organized, indexed structure in memory.

Key Features of Lookup Collections

1. One-to-Many Mapping

A standard dictionary throws an exception if you attempt to insert duplicate keys. An ILookup handles multiple identical keys by design, automatically grouping all items that share the same key into an internal collection.

2. Immediate Execution

Unlike most LINQ query operators (such as Where, Select, or GroupBy) that use deferred execution, ToLookup uses immediate execution. The source data is completely enumerated and evaluated at the exact point ToLookup is called.

3. Immutability

The ILookup<TKey, TElement> interface does not provide Add, Remove, or Clear methods. Once created, the collection cannot be modified. This makes lookups inherently thread-safe for concurrent read operations.

4. Safe Indexer Access

When accessing a non-existent key in a standard Dictionary<TKey, TValue>, the runtime throws a KeyNotFoundException. In contrast, querying an ILookup with a key that does not exist safely returns an empty sequence (Enumerable.Empty<TElement>()), eliminating the need for ContainsKey checks.

ToLookup vs. ToDictionary vs. GroupBy

Feature ToLookup ToDictionary GroupBy
Execution Immediate Immediate Deferred
Key Multiplicity One-to-Many One-to-One (Unique) One-to-Many
Missing Key Access Returns empty sequence Throws KeyNotFoundException N/A (Enumeration only)
Mutability Read-Only Read/Write Read-Only
Key Lookup Indexer Yes (lookup[key]) Yes (dict[key]) No

Practical Code Example

Consider a scenario where you have a list of employees and want to organize them by department for fast querying:

using System;
using System.Collections.Generic;
using System.Linq;

public class Employee
{
    public string Name { get; set; }
    public string Department { get; set; }
}

public class Program
{
    public static void Main()
    {
        List<Employee> employees = new List<Employee>
        {
            new Employee { Name = "Alice", Department = "IT" },
            new Employee { Name = "Bob", Department = "HR" },
            new Employee { Name = "Charlie", Department = "IT" },
            new Employee { Name = "Diana", Department = "Marketing" }
        };

        // Create the lookup
        ILookup<string, string> employeesByDept = employees.ToLookup(
            e => e.Department,      // Key selector
            e => e.Name             // Element selector
        );

        // Access elements using the indexer
        Console.WriteLine("IT Department:");
        foreach (string name in employeesByDept["IT"])
        {
            Console.WriteLine($"- {name}");
        }

        // Accessing a key that does not exist returns an empty collection
        Console.WriteLine("\nFinance Department Count:");
        Console.WriteLine(employeesByDept["Finance"].Count()); // Outputs: 0

        // Iterate through all groups
        Console.WriteLine("\nAll Departments:");
        foreach (IGrouping<string, string> group in employeesByDept)
        {
            Console.WriteLine($"{group.Key}: {string.Join(", ", group)}");
        }
    }
}

When to Use ToLookup

Use ToLookup when:

  • You need to query grouped data repeatedly using index-based lookups (lookup[key]).
  • Your dataset contains multiple elements per key.
  • You want to avoid null checks or exceptions when querying keys that might not exist in the collection.
  • You need an immutable snapshot of grouped data evaluated immediately.