Updated on
Dictionary<TKey, TValue> has no built-in method that merges dictionaries, so we merge them ourselves: with a foreach loop that copies every key-value pair into a new dictionary, or with LINQ methods such as Concat(), GroupBy(), ToLookup() and Union().
These approaches treat a key that appears in both dictionaries differently, and they don’t run equally fast. Let’s compare both to help you choose the best option for your specific use case.
Let’s dive in!
What Is a Dictionary in .NET?
Dictionary is a fundamental data structure that enables efficient storage and retrieval of key-value pairs. They provide a flexible and powerful mechanism to store data elements based on unique keys.
There are situations where it becomes necessary to combine or merge multiple dictionaries into a single unified structure. Dictionary merging involves the process of combining key-value pairs from different dictionaries, enabling enhanced data manipulation, aggregation, or consolidation.
Merging Dictionaries Project Setup
We will demonstrate a variety of methods, such as concatenation, iteration, grouping, union, and more, providing an overview of their usage and performance considerations. All these methods can merge dictionaries with the same type of key-value pairs.
Let’s begin by creating a new console app using the dotnet new console -n app command in the command window, and let’s create two sample dictionaries:
var dictionaryA = new Dictionary<int, string>()
{
{ 1, "Monday" },
{ 2, "Tuesday" },
{ 3, "Wednesday" }
};
var dictionaryB = new Dictionary<int, string>()
{
{ 4, "Thursday" },
{ 5, "Friday" },
{ 6, "Saturday" },
{ 7, "Sunday" }
};
Both dictionaries have int keys and string values. The keys represent the numeric values of the days of the week, while the values correspond to the names of the days. We initialize the dictionaries using collection initializer syntax, which allows concise addition of key-value pairs during creation.
Use foreach Iteration for Merging Dictionaries
First, we will simply iterate each dictionary through each key-value pair to create the new dictionary. We put this method, and every merge method after it, in a new Methods class in its own Methods.cs file:
public Dictionary<TKey, TValue> ForEachMethod<TKey, TValue>(
params Dictionary<TKey, TValue>[] dictionaries)
where TKey : notnull
{
var mergedDictionary = new Dictionary<TKey, TValue>();
foreach (var dictionary in dictionaries)
{
foreach (var kvp in dictionary)
{
if (!mergedDictionary.ContainsKey(kvp.Key))
{
mergedDictionary[kvp.Key] = kvp.Value;
}
}
}
return mergedDictionary;
}
Here, we define the ForEachMethod() method that merges multiple dictionaries into a single Dictionary object. This method is generic, with type parameters TKey and TValue, so it works with any key and value types, as long as all the dictionaries we pass share them. The where TKey : notnull constraint is the same one Dictionary<TKey, TValue> declares for its keys, and without it the compiler warns on every method in this article. It accepts a variable number of dictionaries as input using the params keyword, allowing for flexibility in the number of dictionaries to merge.
Inside the method, we create a mergedDictionary object to store the merged key-value pairs. By using nested foreach loops, we iterate over each dictionary and its key-value pairs.
For each key-value pair, we check if the key already exists in the mergedDictionary. If the key is not present, we add the pair to the mergedDictionary. We perform this check to keep the first value we examine, in case of duplicate keys. Otherwise, if we omit it, we assign the last value of the same key we find. That’s how the indexer works: Microsoft Learn’s Dictionary<TKey,TValue>.Add page says that if the key already exists, “setting the Item[] property overwrites the old value”. On .NET Core 2.0 and later, we can also check for the key and add the pair in one call with TryAdd(), which keeps the first value just like our check does.
Finally, we return the mergedDictionary, containing all unique keys and their corresponding values from the input dictionaries.
We print the returned dictionary and we end up with the following:
Key: 1, Value: Monday Key: 2, Value: Tuesday Key: 3, Value: Wednesday Key: 4, Value: Thursday Key: 5, Value: Friday Key: 6, Value: Saturday Key: 7, Value: Sunday
We can see that the dictionaries are successfully merged into one.
Merge Dictionaries With Aggregate() And Concat()
Next, we can make use of the Concat() method of LINQ:
public Dictionary<TKey, TValue> ConcatMethod<TKey, TValue>(params Dictionary<TKey, TValue>[] dictionaries)
where TKey : notnull
{
var mergedDictionary = dictionaries.Aggregate((dict1, dict2) =>
dict1.Concat(dict2).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
);
return mergedDictionary;
}
Here, we define a method called ConcatMethod() that uses the Aggregate() extension with a lambda expression.
The Aggregate() method is a powerful LINQ extension method that helps us perform an iterative computation on a sequence of elements. The overload we use here takes only a lambda function that defines the logic for combining the elements, and it starts from the first dictionary in the array. Another overload also accepts an initial (seed) value.
The lambda function within the Aggregate() method takes two arguments, the accumulated value and the current element from the collection. It performs an operation on these values and returns a new result, which becomes the accumulated value for the next iteration. We continue this process until we iterate through all elements in the collection, resulting in a single value.
Here, within our lambda expression, we concatenate each dictionary and convert it into a new dictionary using the Concat() and ToDictionary() methods. We should note here that this method has as a prerequisite that the keys in the dictionaries are unique across the given dictionaries. If they aren’t, ToDictionary() throws an ArgumentException with the message “An item with the same key has already been added”, even when both dictionaries hold the same value for that key. Also, because Aggregate() has no seed here, calling ConcatMethod() with no dictionaries at all throws an InvalidOperationException, and calling it with a single dictionary returns that same dictionary instance instead of a copy.
Finally, we return the mergedDictionary as the result.
Use SelectMany() And GroupBy() To Merge Dictionaries
Next, let’s use another LINQ extension method:
public Dictionary<TKey, TValue> GroupByMethod<TKey, TValue>(
params Dictionary<TKey, TValue>[] dictionaries)
where TKey : notnull
{
var mergedDictionary = dictionaries.SelectMany(dict => dict)
.GroupBy(kvp => kvp.Key)
.ToDictionary(group => group.Key, group => group.First().Value);
return mergedDictionary;
}
Here we create a method called GroupByMethod() that merges multiple dictionaries into a single one by grouping the key-value pairs based on their keys.
First, we flatten the dictionaries into a sequence of key-value pairs using the SelectMany() method. Then, we group this sequence by the key, resulting in a collection of groups where each group represents a unique key.
We convert these groups into a dictionary using the ToDictionary() method, where the key of each group becomes a key in the resulting dictionary, and the value is the value of the first key-value pair within that group.
Use of ToLookup() To Combine Dictionary Collections
Also, we can use LINQ’s ToLookup() method approach:
public Dictionary<TKey, TValue> LookupMethod<TKey, TValue>(params Dictionary<TKey, TValue>[] dictionaries)
where TKey : notnull
{
var mergedDictionary = dictionaries.SelectMany(dict => dict)
.ToLookup(pair => pair.Key, pair => pair.Value)
.ToDictionary(group => group.Key, group => group.First());
return mergedDictionary;
}
With the SelectMany() method we combine all the key-value pairs from each dictionary into a single sequence.
Next, we use the ToLookup() method to group the key-value pairs by their keys, creating a lookup collection where each key is associated with a collection of corresponding values.
Finally, the lookup collection is converted into a dictionary using the ToDictionary() method. For each group in the lookup collection, the key of the group becomes a key in the resulting dictionary, and the value is the first value in the collection that associates with that key.
Merge Dictionaries With LINQ’s Union()
Next, we can use the Union() extension method of LINQ:
public Dictionary<TKey, TValue> UnionMethod<TKey, TValue>(params Dictionary<TKey, TValue>[] dictionaries)
where TKey : notnull
{
var mergedDictionary = dictionaries.Aggregate((result, dict) =>
{
return result.Union(dict).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
});
return mergedDictionary;
}
Here, for each iteration in the Aggregate() method, we use the Union() method to combine the key-value pairs from the accumulated result and the current dictionary.
The Union() method returns a sequence of unique key-value pairs. Microsoft Learn’s Enumerable.Union page says it plainly: “This method excludes duplicates from the return set.”
But what counts as a duplicate here? A whole KeyValuePair, so both the key and the value have to match. If two dictionaries hold the same key with the same value, Union() keeps one pair and the merge works. If they hold the same key with different values, both pairs survive Union(), and ToDictionary() throws an ArgumentException.
Use Lists to Merge Dictionaries
Our next approach is to use the List collection:Â
public Dictionary<TKey, TValue> UsingListsMethod<TKey, TValue>(
params Dictionary<TKey, TValue>[] dictionaries)
where TKey : notnull
{
var keys = new List<TKey>();
var values = new List<TValue>();
foreach (var dictionary in dictionaries)
{
foreach (var kvp in dictionary)
{
keys.Add(kvp.Key);
values.Add(kvp.Value);
}
}
var mergedDictionary = keys.Zip(values, (key, value) => new KeyValuePair<TKey, TValue>(key, value))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return mergedDictionary;
}
Here, we create two List objects. One, named keys, to store the keys, and another named values, to store the values.
We iterate over each dictionary and its key-value pairs using nested foreach loops, adding the key to the keys list, and the value to values.
Then, we use the Zip() method to combine the keys and values into a sequence of KeyValuePair objects. The Zip() method is useful when we need to apply a function to the elements of two sequences and produce a sequence of the results. Here, we use a lambda expression as the selector function for each pair of corresponding elements, using the List collection for storing the data, as it guarantees the order of the data.
Finally, we apply the ToDictionary() method to convert the sequence of key-value pairs into a dictionary. For this reason, if there is a duplicate key between our dictionaries, this method throws an error.
What Happens to Duplicate Keys When We Merge Dictionaries?
Our six methods don’t agree on what to do with a key that appears in more than one dictionary, and for real code that matters more than any benchmark number.
The ForEachMethod(), GroupByMethod() and LookupMethod() methods keep the first value they meet and silently drop the rest. If we remove the ContainsKey() check from the loop, the indexer takes over and the last value wins instead.
The ConcatMethod() and UsingListsMethod() methods throw an ArgumentException on any repeated key, even when both dictionaries hold the same value for it.
The UnionMethod() method sits in between. It throws only when the same key comes with different values, because identical pairs disappear before ToDictionary() ever sees them.
So before we pick a method, we should decide what a clash means for our data. If the second dictionary holds newer values, we want the last one to win. If a clash means a bug somewhere upstream, we want the exception.
| Method | Same key, different values | Same key, same value | No dictionaries passed |
|---|---|---|---|
ForEachMethod() | Keeps the first value | Keeps the value | Returns an empty dictionary |
ForEachMethod() without the ContainsKey() check | Keeps the last value | Keeps the value | Returns an empty dictionary |
ConcatMethod() | Throws ArgumentException | Throws ArgumentException | Throws InvalidOperationException |
GroupByMethod() | Keeps the first value | Keeps the value | Returns an empty dictionary |
LookupMethod() | Keeps the first value | Keeps the value | Returns an empty dictionary |
UnionMethod() | Throws ArgumentException | Keeps the value | Throws InvalidOperationException |
UsingListsMethod() | Throws ArgumentException | Throws ArgumentException | Returns an empty dictionary |
The empty-input column only bites when we call these methods with a list of dictionaries that might be empty: the two Aggregate() versions need at least one dictionary to start from.
Compare the Methods to Merge Dictionaries
We will evaluate these methods to find the most efficient one in terms of speed, with the benchmark class that uses the BenchmarkDotNet package (dotnet add package BenchmarkDotNet). Let’s create two helper methods to set up our benchmark scenario for testing dictionary merging performance:
using BenchmarkDotNet.Attributes;
private readonly Dictionary<int, string> _dictionaryA = [];
private readonly Dictionary<int, string> _dictionaryB = [];
[GlobalSetup]
public void Setup()
{
// Generate random values for dictionaryA
for (var i = 0; i < 100; i++)
{
var randomValue = GenerateRandomValue();
_dictionaryA.Add(i, randomValue);
}
// Generate random values for dictionaryB
for (var i = 100; i < 200; i++)
{
var randomValue = GenerateRandomValue();
_dictionaryB.Add(i, randomValue);
}
}
private string GenerateRandomValue()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, 5).Select(s => s[Random.Shared.Next(s.Length)]).ToArray());
}
We declare two empty dictionaries, _dictionaryA and _dictionaryB, and the Setup() method fills them. The [GlobalSetup] attribute tells BenchmarkDotNet to run it once for each benchmark method, before that method’s measurements start.
Initially, we run a loop 100 times for each dictionary to populate them. Then for each iteration, we assign the iterator i variable as the key and a random value using the GenerateRandomValue() method, adding the key-value pair to each dictionary.
For _dictionaryA we iterate from 0 to 99, while for _dictionaryB we iterate from 100 to 199. In this way, we ensure that the keys are unique between the two data structures.Â
Using the GenerateRandomValue() method, we generate a random string of five characters using alphanumeric ones. We use the Random.Shared static instance to generate random characters. In the method, we use the Enumerable.Repeat() method to repeat the character string multiple times and then select a random character from each repetition using the Random.Shared.Next() method.
Finally, we concatenate the resulting characters into a string and then we return them.
Benchmark Results
Let’s benchmark the proposed methods by merging the generated dictionaries, and check the result:
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 | Error | StdDev | Median | Rank | Gen0 | Gen1 | Allocated | |----------------- |----------:|----------:|----------:|----------:|-----:|-------:|-------:|----------:| | ForEachBenchmark | 3.422 μs | 0.0679 μs | 0.1461 μs | 3.395 μs | 1 | 2.6703 | 0.1755 | 21.83 KB | | ConcatBenchmark | 5.355 μs | 0.0457 μs | 0.0427 μs | 5.346 μs | 2 | 0.8392 | 0.0153 | 6.88 KB | | ListsBenchmark | 7.609 μs | 0.0969 μs | 0.0907 μs | 7.625 μs | 3 | 3.4637 | 0.1678 | 28.35 KB | | LookupBenchmark | 12.479 μs | 0.2422 μs | 0.2692 μs | 12.420 μs | 4 | 3.4485 | 0.2441 | 28.23 KB | | GroupByBenchmark | 13.682 μs | 0.2673 μs | 0.5580 μs | 13.468 μs | 5 | 5.4932 | 0.6561 | 44.94 KB | | UnionBenchmark | 17.461 μs | 0.3388 μs | 0.4405 μs | 17.363 μs | 6 | 6.1035 | 0.6714 | 49.91 KB |
On .NET 10, the simple loop comes first: BenchmarkDotNet gives it rank 1 at about 3.4 microseconds to merge our two dictionaries, with Concat() second at about 5.4 microseconds. The ListsBenchmark() method comes next, and the methods that group the pairs first, LookupBenchmark() and GroupByBenchmark(), take roughly four times as long as the loop. UnionBenchmark() is the slowest.
The ForEachBenchmark() method wins with nothing more than a loop and a dictionary. In the memory column, though, ConcatBenchmark() allocates the least, about 6.9 KB against about 21.8 KB for the loop.
Why would a LINQ chain allocate less than a plain loop? Our loop starts from an empty dictionary, so it has to grow and copy its internal storage several times as we add 200 keys. ToDictionary() can ask the concatenated sequence how many pairs it holds and size the new dictionary once.
The GroupByBenchmark() method uses LINQ’s GroupBy() method to group key-value pairs by keys. This grouping operation involves creating additional data structures to organize and group the elements based on the keys, which may contribute to an increase in memory usage.
Lastly, the method that uses the LINQ Union() method involves creating intermediate collections or enumerators to perform the merging operation, resulting in greater memory usage.
Mind you, these numbers come from merging two dictionaries of 100 keys each, on one machine. At this size every method finishes in under 20 microseconds, so in most code the duplicate-key behaviour we covered earlier is the better reason to pick one. If merging sits on a hot path with much larger dictionaries, we should benchmark our own data.
Conclusion
In this article, we explored various different approaches that allow us to merge dictionaries in C#. We saw that the methods split on duplicate keys first: the loop, GroupBy() and ToLookup() keep the first value, Concat() and the list approach throw, and Union() throws only when the values differ. Then we benchmarked them on .NET 10, where the simple loop came first and Concat() second.
If you need to join other collections, the same question comes up when we merge and concatenate arrays or combine two lists.
Tested with .NET 10 (SDK 10.0.302, runtime 10.0.10), NUnit 5.0.0 and BenchmarkDotNet 0.15.8.
