Updated on

To update a value in a dictionary, assign to it through the indexer: toppings["pepperoni"] = 8;. If the key is already there, the old value is replaced, and if it is not, the key and the value are added. A set never fails because a key is missing.

The trouble starts when the new value depends on the old one. toppings[key] = toppings[key] + 1; reads before it writes, and that read throws KeyNotFoundException when the key is absent. Most of the defensive code around dictionary updates exists to guard that read.

The rest of this article covers updates where we can’t be sure the key exists, and how to make them without hashing the same key three times.

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

How Do We Create a Dictionary to Update?

We’ll create a ToppingsDictionary class with one property called Toppings, declared as public Dictionary<string, int> Toppings { get; }, and set it in the class constructor:

Toppings = new Dictionary<string, int>()
{
    { "pepperoni", 4 },
    { "meatball", 8 },
    { "olive", 0 }
};

This property holds a collection of toppings as the key and the total number of that topping as a value. We initialize the collection with some toppings to start with.

How Do We Update a Dictionary Value With the Indexer?

We update a dictionary value by assigning to its indexer. Toppings["pepperoni"] = 8; stores 8 under that key, and we don’t need to call a method or check anything first.

What the assignment does depends on whether the key is already in the dictionary. Microsoft Learn covers both cases: when the key exists, “the value associated with that key is replaced by the assigned value”, and when it does not, “the key and value are added to the dictionary”.

So a set either replaces a value or adds a new entry. That makes the indexer the right default for writing a value we already have in hand, and it is why the indexer never throws for a missing key when we assign to it.

The Add method behaves the opposite way. It refuses a key that already exists and throws ArgumentException, which makes it wrong for an update and right when a duplicate key is a bug we want reported.

Both cases are documented on the Dictionary indexer reference page. Now, let’s create a method that updates the dictionary value with the indexer:

public void AddToppings(string toppingType, int amount)
{
    Toppings[toppingType] = Toppings[toppingType] + amount;
}

In this method, we use the toppingType string in the Toppings dictionary indexer to get the value for that key. Then we add the amount passed into the method to the value we get back from the dictionary.

Let’s create a new instance of our ToppingsDictionary class and call our AddToppings method:

var toppingsDictionary = new ToppingsDictionary();
toppingsDictionary.AddToppings("pepperoni", 4);
toppingsDictionary.AddToppings("olive", 3);

Now, let’s print each key and value from our dictionary to see how the values have been updated. A foreach loop is the simplest of the ways to loop through a dictionary:

foreach (var topping in toppingsDictionary.Toppings)
{
    Console.WriteLine($"{topping.Key} = {topping.Value}");
}

We can see that this is the output produced:

pepperoni = 8
meatball = 8
olive = 3

What Are the Risks of Updating a Dictionary Value With the Indexer?

Setting a value through the indexer is safe. Reading through it is where the risk lies, and the two get confused because a compound update does both in one line.

Toppings[toppingType] = Toppings[toppingType] + amount; is a read followed by a write. The read on the right runs first, and when toppingType is not in the dictionary, it throws KeyNotFoundException before the assignment ever happens. Learn documents that exception for the get. The set on the left would have added the missing key.

So the danger comes from depending on a value that may not be there, and any update that reads the old value before writing the new one carries it.

The same line has a cost we can’t see in the output. It hashes the same key twice, once to read and once to write. Guarding it with ContainsKey first makes three hashes for what is one update, and the rest of this article is about getting that number down.

Let’s pass a topping type that doesn’t exist in our dictionary yet to the indexer version of our AddToppings method:

toppingsDictionary.AddToppings("jalapeno", 1);

We get the exception as a result:

System.Collections.Generic.KeyNotFoundException: The given key 'jalapeno' was not present in the dictionary.

How Do We Update a Value Only When the Key Already Exists?

We can check if a key exists in a C# dictionary by using the dictionary’s ContainsKey method. It accepts a key as a parameter and returns true if the key exists in the dictionary and false if the key doesn’t exist.

Let’s update our AddToppings method to make use of the ContainsKey method:

public void AddToppings(string toppingType, int amount)
{
    if (Toppings.ContainsKey(toppingType))
    {
        Toppings[toppingType] = Toppings[toppingType] + amount;
    }
    else
    {
        Toppings.Add(toppingType, amount);
    }
}

Now, when we use our AddToppings method with a key that doesn’t exist, the key and value will be added:

pepperoni = 8
meatball = 8
olive = 3
jalapeno = 1

Our AddToppings method is working when we use it with a topping that doesn’t exist in our dictionary but it’s not very efficient. That’s because we’re accessing the dictionary three times when we update a value for a key that already exists. The first time is when we use the ContainsKey method. The second time is when we get the existing value for the key using the indexer and add our new amount to it. The third time is when we store the new total through the indexer.

We compare the ways to check if a key exists in a C# dictionary in one article, and we measure the cost of looking the same key up twice in another.

Fortunately, there’s a more concise way to check if a dictionary key exists and get the value in C#.

How Do We Add or Update a Dictionary Value in One Call?

TryGetValue combines the check and the read into one call. It returns true and hands the current value back through an out parameter, or returns false and sets that variable to its default value. One hash tells us whether the key exists and what it holds.

Two hashes remain in an update: the TryGetValue call and the indexer set that follows it. That is one fewer than the guarded version, and it is the right shape for most code.

TryAdd handles the case where an existing value must survive. It stores the pair and returns true, or, when the key is already present, does nothing and returns false. Microsoft Learn puts the difference this way: “Unlike the Dictionary indexer, TryAdd doesn’t override the element if the element with the given key exists in the dictionary.”

For the last step, CollectionsMarshal.GetValueRefOrAddDefault brings an add or update down to a single hash.

Let’s update our AddToppings method to replace our ContainsKey usage with the TryGetValue method:

public void AddToppings(string toppingType, int amount)
{
    if (Toppings.TryGetValue(toppingType, out int currentAmount))
    {
        Toppings[toppingType] = currentAmount + amount;
    }
    else
    {
        Toppings.Add(toppingType, amount);
    }
}

Now, we’re checking that the topping already exists in the dictionary and when it does, we declare the value for it in a variable called currentAmount. Then we use the currentAmount variable to add the given amount to it, instead of retrieving the value again with the indexer.

When we only want to add a topping that isn’t on the pizza yet, TryAdd does it without touching an existing value:

public void AddToppingIfMissing(string toppingType, int amount)
{
    if (!Toppings.TryAdd(toppingType, amount))
    {
        Console.WriteLine($"{toppingType} is already on the pizza.");
    }
}

To update with one hash, CollectionsMarshal.GetValueRefOrAddDefault from the System.Runtime.InteropServices namespace finds the key or adds it with a default value, and returns a reference to the value stored in the dictionary:

using System.Runtime.InteropServices;

public void AddToppingsByRef(string toppingType, int amount)
{
    ref var currentAmount = ref CollectionsMarshal
        .GetValueRefOrAddDefault(Toppings, toppingType, out _);

    currentAmount += amount;
}

A new entry starts at default, which is 0 for an int, so currentAmount += amount; is right whether the key was there or not. The GetValueRefOrAddDefault documentation adds one caveat: “Items should not be added to or removed from the Dictionary<TKey,TValue> while the ref TValue is in use.”

Here are the update calls side by side, with the number of times each one hashes the key:

I want toCallIf the key is missingIf the key already existsHashes
Store a value, creating the key if neededdict[key] = value;Adds the key and valueReplaces the value1
Add only, never overwritedict.TryAdd(key, value)Adds it, returns trueDoes nothing, returns false1
Add and be told about a duplicatedict.Add(key, value)Adds itThrows ArgumentException1
Read the current value, then write a new onedict.TryGetValue(key, out var current)Returns false, current is defaultReturns true with the value1, plus 1 for the set
Update in place, one lookup totalCollectionsMarshal .GetValueRefOrAddDefault(dict, key, out var exists)Adds an entry holding default, exists is falseexists is true; assign through the ref1

Every row works on a plain Dictionary<TKey, TValue> that one thread uses at a time.

How Do We Add or Update a Value From Several Threads?

Dictionary<TKey, TValue> is not safe for concurrent writes. Two threads running the read-then-write update above on the same key can both read the same old value and both write back the same new one, and one of the increments is lost. Nothing throws, so nothing tells us the count is wrong.

ConcurrentDictionary<TKey, TValue> exists for that case, and its AddOrUpdate method does this article’s add or update in one call. It takes the key, the value to store when the key is absent, and a function that turns an existing value into the new one.

Microsoft Learn notes that the factory delegates “may be executed multiple times” and are “called outside the locks”, so AddOrUpdate is not atomic with respect to every other operation on the dictionary. The update function should be short and free of side effects, because it can run more than once for a single call.

Our article on ConcurrentDictionary in C# covers the rest of the type. Here, one call adds or updates the pepperoni total:

using System.Collections.Concurrent;

var toppings = new ConcurrentDictionary<string, int>();

toppings.AddOrUpdate("pepperoni", 4, (_, currentAmount) => currentAmount + 4);

The first call adds the key and returns 4, and a second identical call runs the update function and returns 8. The AddOrUpdate reference documents the delegate behavior described above.

Conclusion

In this article, we looked at a few different ways we can update the value stored in a dictionary. We started with the indexer, which adds or replaces a value in one assignment, then guarded the read with ContainsKey, saved a lookup with TryGetValue, added only missing keys with TryAdd, and brought the update down to one hash with CollectionsMarshal.GetValueRefOrAddDefault. When several threads update the same values, ConcurrentDictionary.AddOrUpdate handles the add or update in one call.

Tested with .NET 10.0.10.