How to Add or overwrite a value in ConcurrentDictionary in C#

By FoxLearn 3/12/2025 4:02:31 AM   19
The ConcurrentDictionary is a thread-safe collection, which allows you to safely add, retrieve, or modify items from multiple threads. You can use the indexer to add or overwrite a value easily, but there are times when you might want to handle your operations more selectively depending on your requirements.

Let's explore how to add or overwrite values in a ConcurrentDictionary using different methods: the indexer, TryAdd(), and AddOrUpdate().

Using the Indexer

The indexer provides the simplest way to unconditionally add or overwrite a value. If the key doesn't exist, it will be added; if it does exist, it will be overwritten. The operation is thread-safe, meaning that multiple threads can access and modify the dictionary without causing issues.

var sessionMap = new ConcurrentDictionary<int, UserSession>();

// Add a new session
sessionMap[101] = new UserSession();

// Overwrite an existing session
sessionMap[101] = new UserSession();

This is a straightforward way to manage data, especially if you don't care about the existing value or need to overwrite it every time.

When to Use TryAdd()

The TryAdd() method adds a new key/value pair only if the key doesn't already exist. It returns true if it was successful in adding the item and false if the key was already present. This is useful when you want to ensure that you're not overwriting any existing data.

Consider this scenario where you're managing user sessions, and you want to prevent overwriting an existing session:

if (!sessionMap.TryAdd(sessionId, new UserSession()))
{
    throw new SessionExistsException();
}

Compare that with this thread-unsafe version:

if (!sessionMap.ContainsKey(sessionId))
{
    sessionMap[sessionId] = new UserSession();
}
else
{
    throw new SessionExistsException();
}

In the second example, there’s a race condition because ContainsKey() could return false, but before the session is added, another thread could have already added it. TryAdd() eliminates this issue by ensuring that the addition of a new item is atomic and thread-safe.

When to Use AddOrUpdate()

The AddOrUpdate() method behaves differently. If the key doesn't exist, it adds the key/value pair. But if the key already exists, it calls a provided delegate (updateValueFactory) to modify the existing value. This makes it ideal when you need to update an existing entry based on its current state.

For instance, imagine you are tracking the number of messages a user has sent in a messaging app, and multiple threads are responsible for updating this count:

var messageCountMap = new ConcurrentDictionary<int, int>();

messageCountMap.AddOrUpdate(userId, 1, 
    (key, currentValue) => currentValue + 1);

In this case:

  • If userId doesn’t exist, it sets the value to 1.
  • If userId already exists, it increments the current value by 1.

Warning: updateValueFactory May Run Multiple Times

When multiple threads call AddOrUpdate() concurrently, the delegate (updateValueFactory) could execute multiple times before succeeding. This happens because AddOrUpdate() tries to update the value and, if unsuccessful, retries the operation until it succeeds. If two threads try to update the same key concurrently, they could both execute the delegate repeatedly.

var userMessages = new ConcurrentDictionary<int, int>();
userMessages.TryAdd(1, 0);

var allTasks = new List<Task>();

for (int i = 0; i < 10; i++)
{
    int taskId = i;
    allTasks.Add(Task.Run(() =>
    {
        userMessages.AddOrUpdate(1, 0, (key, currentValue) =>
        {
            Console.WriteLine($"Task {taskId} - Current value: {currentValue}");
            return currentValue + 1;
        });
    }));
}

await Task.WhenAll(allTasks);

Console.WriteLine($"Final message count for user 1: {userMessages[1]}");

In this example, multiple tasks concurrently attempt to update the message count for userId = 1. You’ll see that the delegate (updateValueFactory) executes multiple times, and each thread works with the current value at the time it accesses it, resulting in multiple increments.