Updated on
A foreach loop is how we iterate a Dictionary<TKey, TValue> in C#. Each pass hands us one KeyValuePair<TKey, TValue>, and the shortest way to read it is to deconstruct it in the loop header: foreach (var (key, value) in monthsInYear).
Everything else is a variation on that. We can loop over Keys or Values alone when we need one half of the entry, reach a single position with ElementAt(), or hand the whole dictionary to PLINQ when the work inside the loop is heavy enough to pay for the threads.
The type itself has its own article, Dictionary in C#. Here we only walk through one.
How Do We Read the Key and the Value in a foreach Loop?
A foreach loop over a Dictionary<TKey, TValue> gives us one entry per pass, and that entry is a KeyValuePair<TKey, TValue>. Microsoft Learn puts it plainly: “For purposes of enumeration, each item in the dictionary is treated as a KeyValuePair<TKey,TValue> structure representing a value and its key.”
So there are two ways to take the entry apart, and both are one line.
We can keep the pair whole and read its properties, which is what month.Key and month.Value do below.
Or we can deconstruct it in the loop header and name the two halves right away: foreach (var (key, value) in monthsInYear). KeyValuePair<TKey, TValue> has had a Deconstruct(out TKey key, out TValue value) method since .NET Core 2.0, so this form works on every supported .NET version, though not on .NET Framework.
The loop also promises no order. The documentation is explicit that the order in which the items are returned is undefined, so no loop body should assume the entries arrive the way we added them.
Both statements come from Microsoft Learn’s Dictionary<TKey,TValue> reference. Let’s define the Dictionary object that we are going to use throughout the article:
var monthsInYear = new Dictionary<int, string>
{
{1, "January" },
{2, "February" },
{3, "March" },
{4, "April" }
};
Next, let’s use this Dictionary to loop through all the entries with a foreach loop. Like every SubDictionary method in this article, it is a static method of the Program class, and Main() calls it with monthsInYear:
public static void SubDictionaryUsingForEach(Dictionary<int, string> monthsInYear)
{
foreach (var month in monthsInYear)
{
Console.WriteLine($"{month.Key} : {month.Value}");
}
}
We can also implement foreach loop using a KeyValuePair<TKey,TValue> structure. We can retrieve multiple fields from an object in a single operation (deconstruction) by assigning the selected values to individual variables. Then, we can use type inference when deconstructing inside a foreach loop, which lets us unpack key and value in a single operation.
There are several ways to utilize this.
We can either get only the individual key({month number}), and value({month name}) items or access them as a pair:
public static void SubDictionaryKeyValuePair(Dictionary<int, string> monthsInYear)
{
foreach (KeyValuePair<int, string> entry in monthsInYear)
{
Console.WriteLine($"{entry.Key} : {entry.Value}");
}
foreach (var (key, value) in monthsInYear)
{
Console.WriteLine($"{key} : {value}");
}
}
When we need the entries in a particular order, we sort them first. Our article on sorting a dictionary by value shows how.
How Do We Iterate Only the Keys or Only the Values?
A dictionary exposes its two halves separately through the Keys and Values properties, and each one is a plain foreach.
foreach (var monthNumber in monthsInYear.Keys) walks the keys, and foreach (var monthName in monthsInYear.Values) walks the values.
Neither property copies anything. Microsoft Learn is explicit that the returned collection “is not a static copy; instead, the Dictionary<TKey,TValue>.KeyCollection refers back to the keys in the original Dictionary<TKey,TValue>”, so reading Keys costs us almost nothing and changes to the dictionary show up in it.
The two collections also line up. The documentation states that the order of the keys “is unspecified, but it is the same order as the associated values” returned by Values, so the first key belongs to the first value even though neither sequence is sorted.
When we reach for Keys or Values, the next reader of the code knows we need one half of the entry and not both.
Both quotes are from the Dictionary<TKey,TValue>.Keys reference. Here are both loops over our monthsInYear dictionary:
public static void SubDictionaryUsingKeys(Dictionary<int, string> monthsInYear)
{
foreach (var monthNumber in monthsInYear.Keys)
{
Console.WriteLine(monthNumber);
}
}
public static void SubDictionaryUsingValues(Dictionary<int, string> monthsInYear)
{
foreach (var monthName in monthsInYear.Values)
{
Console.WriteLine(monthName);
}
}
Going the other way, from a value back to its key, is a different problem. We compare the options in Fastest Way to Get a Dictionary Key by Value in C#.
Can We Change a Dictionary While We Iterate It?
Removing entries is safe on modern .NET. Adding them is not.
Microsoft Learn states it for Remove() in one sentence: “.NET Core 3.0+ only: This mutating method may be safely called without invalidating active enumerators on the Dictionary<TKey,TValue> instance.” So on .NET Core 3.0 and every release since, we can walk the dictionary and drop entries as we go, without copying the keys into a list first.
The documentation follows that sentence with a warning: it “does not imply thread safety”. This is fine on one thread and unsafe on two.
Adding an entry is the other story. Any successful add invalidates the enumerator, whether or not the dictionary has to grow, and the next pass of the loop throws an InvalidOperationException: “Collection was modified; enumeration operation may not execute.” Overwriting a key that is already there is fine, because nothing is added. When we need to add while reading, we collect what we want first and add after the loop ends.
Both documentation quotes are from the Dictionary<TKey,TValue>.Remove() reference. This method removes every month whose name starts with J while it walks the dictionary, and leaves February, March and April behind:
public static void SubDictionaryRemoveWhileIterating(Dictionary<int, string> monthsInYear)
{
foreach (var (monthNumber, monthName) in monthsInYear)
{
if (monthName.StartsWith('J'))
{
monthsInYear.Remove(monthNumber);
}
}
}
Overwriting through the indexer is safe inside the loop for the same reason, and our article on how to update the value stored in a dictionary covers the ways to do it.
Why Is a for Loop With ElementAt() Slow on a Dictionary?
Because ElementAt() is not an indexer. A Dictionary<TKey, TValue> has no positional access to offer, so LINQ falls back on walking the sequence from the start and counting until it reaches the position we asked for.
Inside a for loop that visits every entry, that turns one pass into many. The loop runs once per entry, and each call walks part of the dictionary again, so the total work grows with the square of the size rather than in step with it.
The benchmark further down measures exactly that, and its numbers look like typos until this is said out loud.
There is a second reason to keep this form for the rare case. A dictionary has no stable index. Position 3 is an artefact of the current internal layout, and adding or removing entries can move an entry somewhere else entirely.
So ElementAt() earns its place for reaching one known position, and foreach is the answer for reaching all of them.
The figure shows the difference: one walk against a new walk for every index.

To get the KeyValuePair<TKey,TValue> at a specified index, we call the ElementAt() method for every entry in the for loop:
public static void SubDictionaryForLoop(Dictionary<int, string> monthsInYear)
{
for (int index = 0; index < monthsInYear.Count; index++)
{
KeyValuePair<int, string> month = monthsInYear.ElementAt(index);
Console.WriteLine($"{month.Key} : {month.Value}");
}
}
The same comparison across arrays, lists and dictionaries is in our article on comparing for and foreach loops. When we only need one position, our article on getting an item by index from a dictionary covers the options.
When Does ParallelEnumerable.ForAll() Beat foreach?
There is another way we can iterate through the dictionary, and that’s with the parallel ForAll() method. Splitting the work across threads only pays when the work inside the loop is expensive enough to cover the cost of the split, which is why this method loses badly on a plain read and can win on a heavier body.
This method incorporates parallel processing of key-value pairs in the Dictionary:
public static void SubDictionaryParallelEnumerable(Dictionary<int, string> monthsInYear)
{
monthsInYear.AsParallel()
.ForAll(month => Console.WriteLine($"{month.Key} : {month.Value}"));
}
The output arrives in whatever order the threads finish, and a dictionary has no defined enumeration order to begin with.
AsParallel() is the entry point to PLINQ, which we cover in Understanding PLINQ in .NET.
Note that parallel iteration here means many threads reading one dictionary. If several threads also need to write to it, we need a ConcurrentDictionary instead.
Which Way of Iterating a Dictionary Is Fastest?
For an ordinary loop body, foreach is fastest, and it is not close.
The reason is that foreach asks the dictionary for its own enumerator and walks the storage once. The for loop calls ElementAt() on every pass and pays for a fresh walk each time, and the parallel form pays to split the work and hand it to a thread pool before any of our own code runs.
That ranking flips only when the body of the loop becomes expensive and the work splits cleanly between threads. In our second benchmark each thread builds its own part of a string and the parts are joined at the end, and there AsParallel().ForAll() pulls ahead from 1,000 entries while still losing at 100.
Iterating a dictionary is cheap. Whatever we do per entry decides whether threads are worth it.
Measure before believing any of this for a specific loop body, including this one.
Here is every form from this article on one screen:
| To iterate... | Write | Cost and caveats |
|---|---|---|
| every key/value pair | foreach (var (key, value) in dict) | The default. Deconstruction, available since .NET Core 2.0 |
| every pair, as one object | foreach (KeyValuePair<int, string> pair in dict) | Same loop, same cost; read pair.Key and pair.Value |
| the keys only | foreach (var key in dict.Keys) | A live view, not a copy |
| the values only | foreach (var value in dict.Values) | A live view, not a copy |
| one position | dict.ElementAt(index) | Walks from the start every call. Never put it inside a for loop over the whole dictionary |
| in parallel | dict.AsParallel().ForAll(pair => ...) | The loop body must be thread safe. Only worth it when that body is expensive |
| while removing entries | foreach (var (key, _) in dict) { if (...) dict.Remove(key); } | Remove() does not invalidate an active enumerator on .NET Core 3.0 and later |
To compare the speed of the three loops, we remove the Console.WriteLine() call and hand each value to BenchmarkDotNet’s Consumer, so the compiler cannot throw the loop body away:
private readonly Consumer _consumer = new();
[Benchmark]
[ArgumentsSource(nameof(SampleData))]
public void WhenDictionaryUsingForEach(Dictionary<int, string> dictionaryData, string numberOfItems)
{
foreach (var testValue in dictionaryData)
{
_consumer.Consume(testValue.Value);
}
}
[Benchmark]
[ArgumentsSource(nameof(SampleData))]
public void WhenDictionaryUsingForLoop(Dictionary<int, string> dictionaryData, string numberOfItems)
{
for (int i = 0; i < dictionaryData.Count; i++)
{
var item = dictionaryData.ElementAt(i);
_consumer.Consume(item.Value);
}
}
[Benchmark]
[ArgumentsSource(nameof(SampleData))]
public void WhenDictionaryParallelEnumerable(Dictionary<int, string> dictionaryData, string numberOfItems)
{
dictionaryData.AsParallel().ForAll(testValue => _consumer.Consume(testValue.Value));
}
We’re going to test for three different scenarios or rather with the different number of elements in the dictionary: 100, 1000, and 10000:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update) AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores .NET SDK 10.0.302 [Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 | Method | dictionaryData | numberOfItems | Mean | Error | StdDev | |--------------------------------- |--------------------- |-------------- |-----------------:|----------------:|-----------------:| | WhenDictionaryUsingForEach | Syste(...)ring] [67] | 100 | 294.4 ns | 6.93 ns | 19.56 ns | | WhenDictionaryUsingForLoop | Syste(...)ring] [67] | 100 | 19,443.4 ns | 365.29 ns | 375.13 ns | | WhenDictionaryParallelEnumerable | Syste(...)ring] [67] | 100 | 23,033.9 ns | 436.53 ns | 485.20 ns | | WhenDictionaryUsingForEach | Syste(...)ring] [67] | 1000 | 3,206.9 ns | 129.50 ns | 381.85 ns | | WhenDictionaryUsingForLoop | Syste(...)ring] [67] | 1000 | 1,772,630.2 ns | 34,820.57 ns | 61,893.53 ns | | WhenDictionaryParallelEnumerable | Syste(...)ring] [67] | 1000 | 92,954.2 ns | 926.27 ns | 866.43 ns | | WhenDictionaryUsingForEach | Syste(...)ring] [67] | 10000 | 29,854.6 ns | 867.74 ns | 2,544.94 ns | | WhenDictionaryUsingForLoop | Syste(...)ring] [67] | 10000 | 177,826,075.3 ns | 4,222,427.24 ns | 12,182,668.93 ns | | WhenDictionaryParallelEnumerable | Syste(...)ring] [67] | 10000 | 287,603.4 ns | 5,698.59 ns | 6,562.51 ns |
As expected the foreach loop performs very well in all three cases. What’s interesting is that it outperforms the parallel ForAll() method even when the number of items is very high. We can push this even further but 10k is already a big number of items to hold in a dictionary.
The for loop shows the cost the ElementAt() section described. Every tenfold increase in entries makes it about a hundred times slower, from 19.4 microseconds at 100 entries to 177.8 milliseconds at 10,000.
Note that we’ve used a trivial loop body for this test. Speed might vary for different logic we want to execute while iterating the dictionary. But in terms of pure speed, the foreach loop wins the race.
The improvement in ForAll() execution speed is visible though. When iterating 100 elements, foreach loop is 78x faster, but with 10k elements, it’s only 9.6x faster than the parallel ForAll() method. Does this justify the usage of ForAll()? Maybe not in this case, but if we plan to execute a more complicated logic during iteration it might be worth it.
Let’s make it a bit more complicated and change the Consume() call to string concatenation (+=). Many threads cannot safely append to one shared string, so in the parallel version each thread builds its own string in a ThreadLocal<string>, and we join the parts at the end:
[Benchmark]
[ArgumentsSource(nameof(SampleData))]
public string WhenDictionaryUsingForEach(Dictionary<int, string> dictionaryData, string numberOfItems)
{
var result = string.Empty;
foreach (var testValue in dictionaryData)
{
result += testValue.Value;
}
return result;
}
[Benchmark]
[ArgumentsSource(nameof(SampleData))]
public string WhenDictionaryUsingForLoop(Dictionary<int, string> dictionaryData, string numberOfItems)
{
var result = string.Empty;
for (int i = 0; i < dictionaryData.Count; i++)
{
var item = dictionaryData.ElementAt(i);
result += item.Value;
}
return result;
}
[Benchmark]
[ArgumentsSource(nameof(SampleData))]
public string WhenDictionaryParallelEnumerable(Dictionary<int, string> dictionaryData, string numberOfItems)
{
using var partialResults = new ThreadLocal<string>(() => string.Empty, trackAllValues: true);
dictionaryData.AsParallel().ForAll(testValue => partialResults.Value += testValue.Value);
return string.Concat(partialResults.Values);
}
The results for the same three sizes:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update) AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores .NET SDK 10.0.302 [Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 | Method | dictionaryData | numberOfItems | Mean | Error | StdDev | |--------------------------------- |--------------------- |-------------- |---------------:|--------------:|--------------:| | WhenDictionaryUsingForEach | Syste(...)ring] [67] | 100 | 8.296 us | 0.6142 us | 1.811 us | | WhenDictionaryUsingForLoop | Syste(...)ring] [67] | 100 | 29.380 us | 0.9249 us | 2.727 us | | WhenDictionaryParallelEnumerable | Syste(...)ring] [67] | 100 | 30.412 us | 1.1338 us | 3.343 us | | WhenDictionaryUsingForEach | Syste(...)ring] [67] | 1000 | 514.422 us | 49.5451 us | 146.085 us | | WhenDictionaryUsingForLoop | Syste(...)ring] [67] | 1000 | 2,541.958 us | 81.2846 us | 237.111 us | | WhenDictionaryParallelEnumerable | Syste(...)ring] [67] | 1000 | 200.824 us | 3.9872 us | 5.323 us | | WhenDictionaryUsingForEach | Syste(...)ring] [67] | 10000 | 102,109.326 us | 2,019.4020 us | 3,589.485 us | | WhenDictionaryUsingForLoop | Syste(...)ring] [67] | 10000 | 279,067.981 us | 5,529.5733 us | 14,469.931 us | | WhenDictionaryParallelEnumerable | Syste(...)ring] [67] | 10000 | 6,982.889 us | 139.0246 us | 216.445 us |
This time the results favor the parallel ForAll() method from 1,000 elements up: 200.8 microseconds against 514.4 for foreach, and with 10k it clearly wins, at 7.0 milliseconds against 102.1 milliseconds. At 100 elements it still loses, 30.4 microseconds against 8.3.
Part of that win comes from the work getting smaller, on top of being shared out. Appending to a string copies the whole string every time, so one long string gets more expensive with every append. Each thread’s string holds only part of the text, so the threads copy far fewer characters before the final join.
The ThreadLocal<string> keeps the parallel number honest. Writing result += testValue.Value to one shared local from inside ForAll() looks like the same benchmark, but it is a data race: threads overwrite each other’s appends. When we ran that form eight times at 10,000 entries, the result kept between 8,363 and 21,342 of the 98,890 characters, so most of the work was lost.
Conclusion
So if we need to iterate through a dictionary, we can conclude that using a simple foreach method does the job decently in most cases. Parallel execution pays only when the work per entry is expensive and each thread can do its share without touching state the others use, as the string concatenation benchmark shows.
Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.
