If you modify the AsyncDictionary while enumerating its keys/values it throws InvalidOperationException (if the backing dictionary is a Dictionary).
var numbers = new AsyncDictionary<int, int>();
foreach(var number in Enumerable.Range(1, 1000))
await numbers.AddAsync(number, number);
foreach(var number in await numbers.GetKeysAsync())
It's pretty hard to break something that uses a global lock around everything. So this seems pretty thread-safe. But that doesn't answer the question of why you'd want to use this.
Asynchronous calls are useful for things that take a long time, particularly if you can delegate the "waiting" to some low-level event based solution (HTTP requests for example)....
The reason why there is no async API for a dictionary is, that all operations on a dictionary are so fast, that there is no need for asynchronicity.
For concurrent scenarios there is the thread safe variant - the ConcurrentDictionary.
Adding an async API to these dictionaries has absolutely zero value. Rather it increases complexity and reduces performance....
Your implementation has a very intrusive lock for all read and write operations, using the SemaphoreSlim with max concurrency 1.
await _semaphoreSlim.WaitAsync()// <- both read/write operations acquire single mutex
return await Task.Run(async () =>
return await func(_dictionary, keyValuePair);
Cancellation Token Source
You create a token source, and then immediately overwrite it with a new instance, disregarding the previously created instance. Surely, you did not mean to instantiate it twice?
_runningSubscriberCancellationTokenSource = new CancellationTokenSource();
_runningSubscriberCancellationTokenSource = CancellationTokenSource....
Generally, the code is simple and easy to understand.
It's good that you have a dedicated Lock object. You might consider a more expressive name, but in this case I think you can get away with it.
As you have noticed, using the lock means you gain nothing from the ConcurrentDictionary'2. Since this is such a simple interface, I would keep the ...
You can simplify GetOrCreateAsync() a bit, because IMemoryCache has an extension called Microsoft.Extensions.Caching.Memory.GetOrCreateAsync():
public async Task<T> GetOrCreateAsync<T>(object key, Func<ICacheEntry, Task<T>> create)
return await _cache.GetOrCreateAsync(key, create);
Sorry this is a bit of a ramble, but a few things jump out at me:
17?!?! I don't have to tell you about magic numbers! This should probably be configurable... somehow...
Why is Locker static? It doesn't make sense to restrict access to one cache because a totally different cache is currently in use.
Locking the cache while you create an item isn't ...