Updated on

C# has no built-in way to find a dictionary key from its value, so we write one ourselves. Four approaches are in common use: build a reverse dictionary, reverse it with ToLookup(), scan the key-value pairs, or scan the keys.

For a single lookup, scanning the key-value pairs wins, and it allocates nothing while doing it. Reversing the dictionary only pays off when we look keys up repeatedly against a dictionary that is not changing, and we benchmark both cases below.

If it is the other direction we need, a value from a key, that is dict[key], TryGetValue() or GetValueOrDefault(), covered in its own short section below.

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

Can One Value Belong to Several Dictionary Keys in C#?

Yes. A Dictionary<TKey, TValue> enforces uniqueness on keys only. Adding the same key twice throws, but two different keys may hold the same value, and nothing in the type prevents it.

A key has at most one value, so dict[key] can return it. A value may belong to no keys, one key, or fifty, so an honest reverse lookup returns a collection rather than a single key.

Every method here returns only the first matching key. First means first in enumeration order, and a dictionary makes no ordering promise: the order follows insertion history and how internal slots were reused after removals. With duplicate values, the key we get back is arbitrary.

When duplicates are possible and we want all of them, the answer is a filter instead of a search: keep every pair whose value matches, and return those keys.

We build four methods for a reverse dictionary lookup, each in its own section:

Many readers arrive here needing the opposite direction, a value from its key, so we cover that before we build the methods.

How Do We Get a Dictionary Value by Key in C#?

The forward direction is the one the language gives us, and there are three calls for it.

The indexer, dict[key], returns the value and throws KeyNotFoundException when the key is absent. Use it when a missing key is a bug.

dict.TryGetValue(key, out var value) returns false instead of throwing and hands the value back through the out parameter. Use it when a missing key is expected, which is most of the time.

The extension method dict.GetValueOrDefault(key) returns default(TValue) for a missing key, or a fallback we pass as a second argument.

All three are constant time on average: the dictionary hashes the key and goes straight to its bucket, so the size of the dictionary does not matter.

That is exactly what we cannot do in reverse. Values are not hashed and not indexed, so finding a key by its value means looking at the pairs one at a time, and everything below is about doing that as cheaply as possible.

Each of these has a full treatment of its own: checking whether a key exists before you read it, returning a default value from a dictionary, and how a Dictionary<TKey, TValue> stores its pairs in the first place.

A dictionary lookup by key takes one hashed step, while a lookup by value compares every pair.

How Do We Reverse a Dictionary to Look Up a Key?

When we first encounter this task, a quick solution we might consider is to immediately create another dictionary that has the values from our original dictionary as keys and the keys as values. Then, with our original values as keys in this new dictionary, we invoke the TryGetValue() method to retrieve the desired key.

All four methods live in a DictionaryHelper class that takes the dictionary and the value to search for as primary constructor parameters, so dict and value in every snippet below are those two parameters:

public class DictionaryHelper(Dictionary<string, string> dict, string value)
{

This approach is feasible, so let’s implement a method that does this:

public string? UseReverseDictionary()
{
    var reverseDict = new Dictionary<string, string>();
    foreach (var keyValuePair in dict)
    {
        reverseDict.TryAdd(keyValuePair.Value, keyValuePair.Key);
    }

    reverseDict.TryGetValue(value, out var key);

    return key;
}

First, we create a Dictionary<TKey, TValue> object. Then, we iterate through the key-value pairs, adding our original values as keys and the keys as values. Using the TryAdd() method ensures that the addition process goes smoothly without any exceptions when we try to add duplicate keys. If we attempt to add a duplicate key, TryAdd() returns false without modifying the dictionary, and we proceed to the next KeyValuePair.

After that, we call the TryGetValue() method on our reverse dictionary to retrieve the key associated with the specified value. If we find this value in the dictionary, we store its key in the out parameter, key. Otherwise, we store the default value for strings (null) in the key argument. Lastly, we return the key value.

Using a Reverse Lookup

The ToLookup() method gives us another way to reverse our dictionary:

public string? UseReverseLookup()
{
    var reverseLookup = dict.ToLookup(x => x.Value, x => x.Key);

    return reverseLookup[value].FirstOrDefault();
}

Here, we invoke the ToLookup() LINQ extension method on our dictionary. We instruct it to take the values and keys from our input dictionary and enter them as keys and values respectively in a new Lookup<TKey, TElement> instance.

A Lookup is similar to a Dictionary in that it stores keys and their associated values. However, unlike Dictionary objects, a Lookup can map a single key to a collection of values.

Now, after creating our Lookup, we access the collection of keys mapped to our input value and return the first key or null if the value does not exist.

Retrieve a Dictionary Key by Looping Through the KeyValuePairs

The third method iterates through the key-value pairs in our dictionary:

public string? LoopThroughKeyValuePairs()
{
    foreach (var keyValuePair in dict)
    {
        if (keyValuePair.Value == value)
        {
            return keyValuePair.Key;
        }
    }

    return default;
}

In our foreach loop, we go through all the pairs in our dictionary searching for any value that matches our input value. Once we find a match, we return its key. Walking the pairs with foreach is one option among several, and we cover the other ways to iterate a dictionary in a separate article.

If we don’t find a matching value at the end of our loop iteration, we return the default value for strings.

Iterate Over the Keys in the Dictionary

Similarly, we can iterate over the keys in our dictionary:

public string? LoopThroughKeys()
{
    foreach (var key in dict.Keys)
    {
        if (dict[key] == value)
        {
            return key;
        }
    }

    return default;
}

In this case, we pass each key to the indexer to find a value that is the same as our input value. If any key’s value matches, we return the key. Otherwise, we return default. 

How Do the Four Methods Compare on Speed and Memory?

On a dictionary of 100,000 pairs, searching for a value that is not there, the two scans beat both methods that reverse the dictionary.

Scanning the key-value pairs is fastest and allocates nothing. One foreach walks the dictionary’s entries and compares each value.

Scanning the keys takes about 8.5 times longer for the same answer. Each iteration reads a key and then looks that key up again through the indexer, so every item is paid for twice.

Building a reverse dictionary is slower still, about 25 times the pair scan, and it allocates a second copy of the entire dictionary.

Reversing with ToLookup() is slowest and hungriest. A Lookup groups keys per distinct value, so a dictionary with 100,000 distinct values means 100,000 groupings to allocate.

These numbers measure a single lookup, and the two reversing methods spend all of their time building an index they then use exactly once.

We benchmark these methods with the BenchmarkDotNet package. If you aren’t familiar with this package and want to learn how to benchmark methods with it, kindly visit our introduction to benchmarking C# projects.

To ensure the accuracy of the inferences we make from our benchmark, we will run it with a dictionary containing 100000 items and attempt to retrieve the key of a non-existent value. Attempting to retrieve the key of an existing value in our benchmark may produce skewed results, as we cannot guarantee the ordering of pairs in our dictionary. The Dictionary<TKey,TValue> reference on Microsoft Learn says so in one sentence: “The order in which the items are returned is undefined.”

In such scenarios, the value we aim to retrieve might be the first item in our dictionary, and we will obtain it quickly. By searching for a non-existent value, we simulate the scenario in which all four methods must go through every item in our dictionary to retrieve the desired key.

Benchmark Result

To see our benchmark class implementation, please visit the source code.

The run on .NET 10 produced these results:

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                       | Mean              | Gen0      | Gen1      | Gen2     | Allocated  |
|----------------------------- |------------------:|----------:|----------:|---------:|-----------:|
| UsePrebuiltFrozenDictionary  |          1.984 ns |         - |         - |        - |          - |
| UsePrebuiltReverseDictionary |          6.534 ns |         - |         - |        - |          - |
| LoopThroughTheKeyValuePairs  |    173,944.209 ns |         - |         - |        - |          - |
| LoopThroughTheKeys           |  1,483,338.770 ns |         - |         - |        - |          - |
| UseReverseDictionary         |  4,365,652.930 ns |  390.6250 |  367.1875 | 367.1875 |  8452771 B |
| UseReverseLookup             | 27,954,562.740 ns | 1500.0000 | 1468.7500 | 437.5000 | 10897731 B |

The two prebuilt rows at the top time only the lookup against a reverse map built in advance, not the build itself, so we set them aside until the next section.

As we can see from the benchmark results, the fastest way to get a dictionary key by value in C# is by iterating over the key-value pairs in the dictionary. This method also provides the extra advantage of avoiding additional memory allocation and thus potential garbage collection.

After that, the second-fastest way to carry out this task is by iterating over the keys in the dictionary. It allocates nothing either, but it takes about 8.5 times as long as the first method.

Following these methods, we see the method that manually reverses our dictionary. With this method, we experience the second-highest execution time and garbage collection rate due to the significant amount of managed memory it allocates (basically duplicating our original dictionary).

Finally, the slowest way to get a dictionary key by value in C# is by invoking the ToLookup() method to reverse our dictionary. This method consumes the most memory and uses the garbage collector more frequently. This is because we create many keys and their corresponding collection values for each Lookup object.

Which Approach Should We Use, and When?

The comparison answers one question: which method wins when we look up one key, one time. The two reversing methods lose it because they pay to build an index and then use it once.

Change the question to repeated lookups and the answer inverts. A reverse dictionary is built once and answers every later lookup in constant time, while a scan pays up to one pass per call.

So the choice depends on how many lookups one reverse map would serve. For one lookup, or for a dictionary that keeps changing underneath us, we scan the key-value pairs. For many lookups against a stable dictionary, we build the reverse map once and keep it.

Duplicate values decide the shape of that map. With unique values, a Dictionary<TValue, TKey> is enough. With duplicates, ToLookup() is the right tool after all, because it keeps every key per value instead of silently dropping all but one.

The table sets the four approaches side by side, by what one lookup costs and when each one fits.

ApproachCost of one lookupAllocatesUse it when
Scan the key-value pairsOne pass over the dictionaryNothingWe look a key up once, or the dictionary keeps changing
Scan the keysOne pass, plus a hash lookup per itemNothingNever: it does the same work as the pair scan and more
Reverse Dictionary<TValue, TKey>Constant, after a one-off buildA second dictionaryWe look many keys up against a stable dictionary and values are unique
ToLookup()Constant, after a one-off buildA lookup plus a grouping per distinct valueWe need every key for a value, not just the first

Building the reverse dictionary costs about as much as 25 scans (4,365,653 ns against 173,944 ns in the output above), and each later lookup through it takes about 6.5 ns, which is negligible next to either. So somewhere around the 26th lookup the reverse dictionary moves ahead, and its lead grows with every lookup after that.

If the reverse map will never change again, ToFrozenDictionary() trades a slower build for faster reads than a Dictionary<TKey, TValue> on our missing value, 1.98 ns against 6.53 ns per lookup in the output above, because that value is longer than any value in the map. For values the map holds, the frozen dictionary is slower on average.

Conclusion

Scanning the key-value pairs is the fastest of the four methods for a single lookup, and it allocates nothing. For repeated lookups against a dictionary that does not change, a reverse dictionary built once pulls ahead from about the 26th lookup. When values repeat, ToLookup() is the reverse map to build, because it keeps every key that shares a value.

Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.