PROWAREtech
ASP.NET Core: ConcurrentDictionary - What is It and How to Use it?
See also .NET ConcurrentBag class.
A ConcurrentDictionary
in C# is a thread-safe collection that stores key-value pairs, designed for scenarios where multiple threads need to simultaneously add, remove, or modify items. Unlike a regular Dictionary, it provides atomic operations that are safe for concurrent access without explicit locking, making it ideal for producer-consumer patterns or caching scenarios where one needs to ensure data consistency across threads.
A ConcurrentBag
, on the other hand, is an unordered collection of objects that allows duplicate values and is optimized for scenarios where the same thread is both adding and removing items. While both collections are thread-safe, ConcurrentBag
doesn't maintain key-value relationships and excels in situations where one doesn't need to track specific items but rather just need a thread-safe container for objects, like a pool of resources.
The main difference lies in their use cases: ConcurrentDictionary
is best when one needs to maintain unique keys and look up specific values quickly, while ConcurrentBag
is more suitable for scenarios where one's just collecting items and don't care about their order or uniqueness. ConcurrentDictionary
provides O(1) lookup performance, while ConcurrentBag
is optimized for scenarios where the same thread that added an item is likely to remove it.
Example Code
Here's a more complete real-world example showing a thread-safe request rate limiter:
using System.Collections.Concurrent;
public class RateLimiter
{
private readonly ConcurrentDictionary<string, Queue<DateTime>> _requestTimes = new ConcurrentDictionary<string, Queue<DateTime>>();
private readonly int _maxRequests;
private readonly TimeSpan _timeWindow;
public RateLimiter(int maxRequests, TimeSpan timeWindow)
{
_maxRequests = maxRequests;
_timeWindow = timeWindow;
}
public bool CanMakeRequest(string clientId)
{
return _requestTimes.AddOrUpdate(
clientId,
// If client doesn't exist, create new queue with one timestamp
_ => new Queue<DateTime>(new[] { DateTime.UtcNow }),
// If client exists, update their request times
(_, queue) =>
{
// Remove old timestamps outside the window
while (queue.Count > 0 &&
DateTime.UtcNow - queue.Peek() > _timeWindow)
{
queue.Dequeue();
}
// If under limit, add new timestamp
if (queue.Count < _maxRequests)
{
queue.Enqueue(DateTime.UtcNow);
return queue;
}
// Over limit, return unchanged queue
return queue;
}
).Count <= _maxRequests;
}
}
// Usage:
var rateLimiter = new RateLimiter(maxRequests: 100, timeWindow: TimeSpan.FromMinutes(1));
// In the API endpoint
if (rateLimiter.CanMakeRequest(userId))
{
// Process the request
}
else
{
// Return 429 Too Many Requests
}