WaitingDictionary 2.0.0
dotnet add package WaitingDictionary --version 2.0.0
NuGet\Install-Package WaitingDictionary -Version 2.0.0
<PackageReference Include="WaitingDictionary" Version="2.0.0" />
<PackageVersion Include="WaitingDictionary" Version="2.0.0" />
<PackageReference Include="WaitingDictionary" />
paket add WaitingDictionary --version 2.0.0
#r "nuget: WaitingDictionary, 2.0.0"
#:package WaitingDictionary@2.0.0
#addin nuget:?package=WaitingDictionary&version=2.0.0
#tool nuget:?package=WaitingDictionary&version=2.0.0
WaitingDictionary
Represents a thread-safe collection of keys and TaskCompletionSource<T> as values, managed with two methods: WaitAsync and SetAsync.
What is it for
Sometimes a value produced on one thread/task must be delivered to a caller waiting on another. WaitingDictionary<T, V> is a rendezvous point for them:
WaitAsync(key)either returns a result that is already stored under the key, or parks the caller until someone provides it;SetAsync(key, value)either delivers the value to the caller already waiting on the key, or stores it for the next one.
Typical uses: request coalescing (one in-flight fetch per key, all callers await it), signaling between producers and consumers, per-key async locks with a payload.
Installation
dotnet add package WaitingDictionary
Quick start
Just a mock model:
public class Mock
{
public List<Mock> Nodes { get; }
public Mock(params Mock[] mock)
{
(Nodes ??= new List<Mock>()).AddRange(mock);
}
public Mock()
{
}
}
Base example - wait first, set from somewhere else:
using var dict = new WaitingDictionary<int, Mock>();
// Starting waiting Task<Mock> (blocks until completed or cancelled) by key 1337
var result = await dict.WaitAsync(1337, CancellationToken.None);
//
//
// Some operations on other threads, or tasks..
//
//
// Set result by key 1337, it will release the previously started task that got it by WaitAsync
await dict.SetAsync(1337, new Mock());
Another example - set first, the value is stored until someone picks it up:
using var dict = new WaitingDictionary<int, Mock>();
// Set result by key 1337
await dict.SetAsync(1337, new Mock());
//
//
// Some operations on other threads, or tasks..
//
//
// Got result immediately
var result = await dict.WaitAsync(1337, CancellationToken.None);
Behavior rules
- Automatic removal.
SetAsyncandWaitAsyncautomatically remove the element when it is completed. A stored result is consumed byWaitAsyncexactly once. - One waiter per key. If a key already has a waiter in progress, the next
WaitAsyncthrowsInvalidOperationException. The exception surfaces as a faulted task, sinceWaitAsyncis an async method. - One set per key. If a key already holds a completed result, the next
SetAsyncthrowsArgumentException- unless a duplication middleware is registered. - Cancellation never poisons the key. When a wait is canceled by token (or by
Dispose), the dead item is dropped; the followingWaitAsync/SetAsyncfor the same key starts from a fresh item and works normally. - Disposal.
Disposecancels all pending waits, andDisposeis idempotent. Using the dictionary afterwards throwsObjectDisposedException. Register it as a singleton in your DI container.
API
| Member | Description |
|---|---|
Task<TValue> WaitAsync(TKey key, CancellationToken token = default) |
Waits for (or immediately returns) the value stored by SetAsync. |
Task SetAsync(TKey key, TValue value, bool ignoreError = false) |
Completes the pending wait or stores the value for the next one. |
Task<bool> TryRemoveAsync(TKey key) |
Removes the item; a waiter on it is canceled. |
IEnumerable<KVP> Filter(Func<KVP, bool> predicate) |
Snapshot-based enumeration with a predicate. |
int Count / bool ContainsKey(TKey) / bool TryGetValue(TKey, out WaitItem<TValue>) |
Read-only inspection. The dictionary also implements IReadOnlyDictionary<TKey, WaitItem<TValue>>. |
Note: WaitItem<TValue> exposes the underlying TaskCompletionSource<TValue> DelayedTask and the creation timestamp PlaceDateTime (UTC).
Stale items cleanup
A wait that never receives SetAsync and is never canceled would live forever. Set staleItemTimeToLiveMs to let a background job cancel and remove such items (the stored results age the same way):
// Items older than 30 seconds get canceled; cleanup runs every 30 seconds,
// so an item is cleared no earlier than 30 s and no later than 60 s after creation
using var dict = new WaitingDictionary<int, Mock>(staleItemTimeToLiveMs: 30_000);
MiddlewareBuilder
var middlewares =
new MiddlewareBuilder<Mock>()
// Will run on every completion SetAsync
// Default: none
.RegisterCompletionActionInSet(() => Console.WriteLine("Set completed"))
// Will run on every completion WaitAsync
// Default: none
.RegisterCompletionActionInWait(() => Console.WriteLine("Wait completed"))
// Will run on every duplicated element found while executing SetAsync
// Default: throw exception
.RegisterDuplicateActionInSet((old, @new) => new Mock(old, @new)) // merge two values
// Will run on every WaitAsync cancellation
// Default: TrySetCanceled
// hasOwnToken == true: the wait used its own cancellable token,
// false: the cancellation comes from Dispose()
.RegisterCancellationActionInWait((tcs, hasOwnToken) => tcs.SetException(new Exception("Something went wrong")));
var dict = new WaitingDictionary<int, Mock>(middlewares);
The builder is captured as a snapshot when the dictionary is constructed: registering middleware afterwards does not affect an already created dictionary, and each registration can be done only once (second call throws InvalidOperationException).
Design and thread safety
ConcurrentDictionarymakes single operations safe; aNito.AsyncEx.AsyncLockmakes the check-then-act sequences betweenWaitAsync,SetAsync,TryRemoveAsyncand the cleanup job atomic as a whole.- Every
TaskCompletionSourceis created withRunContinuationsAsynchronously, so continuations of a waiter never run synchronously inside the setter's critical section. - The cleanup timer callback never throws into the thread pool and re-validates each item under the lock before touching it.
What's new in 2.0
Breaking changes:
IDictionary<TKey, …>/ICollection<…>implementations are removed (their members mostly threwNotSupportedException). The dictionary now implements onlyIReadOnlyDictionary<TKey, WaitItem<TValue>>+IDisposable; the indexer is read-only and throwsKeyNotFoundExceptionfor a missing key.Add,Clearand the dictionary-styleRemoveare gone - useSetAsync,TryRemoveAsyncandDispose.WaitAsyncno longer throws a stored cancellation into a new waiter, andSetAsyncno longer throwsInvalidOperationExceptionon a canceled ghost item - dead items are replaced transparently.- The
[Obsolete]constructors are removed; use the main constructor with optional arguments. - A canceled wait now also removes its item from the dictionary.
Fixes and improvements:
- Stale-items cleanup no longer crashes the process when an item disappears concurrently, and no longer races with
Wait/Set. Disposeis idempotent and waits for a running cleanup callback.TryRemoveAsyncnow cancels the orphaned waiter instead of letting it hang forever.TaskCompletionSourceis always created withRunContinuationsAsynchronously(avoids running waiter continuations under the internal lock;TaskCreationOptionspassed to the constructor is preserved).System.Runtime.CompilerServices.IsExternalInitpolyfill allows modern C# records onnetstandard2.1.
Development
dotnet build waiting-dictionary.sln -c Release
dotnet test waiting-dictionary.tests -c Release
Library targets netstandard2.1 (LangVersion latest), tests target net10.0 (NUnit 4).
Publishing (maintainers)
Every green push to master publishes the package to nuget.org via NuGet Trusted Publishing (OIDC) - there is no API key stored in the repository. The package version is taken from <Version> in waiting-dictionary.csproj.
To ship a new package: bump <Version> in the csproj, commit and push to master. The publish job in ci.yml runs after a successful build + test, exchanges the job's OIDC token for a short-lived API key with NuGet/login@v1, and pushes with --skip-duplicate.
One-time setup:
- On nuget.org: user menu → Trusted Publishing → Add policy: owner
Drenalol, repositoryWaitingDictionary, workflow fileci.yml(filename only). Environment: leave empty unless the workflow usesenvironment:. - In the GitHub repository: add a
NUGET_USERsecret (your nuget.org profile name, not email) and remove the obsoleteNUGET_AUTH_TOKENsecret.
License
MIT (c) Bogdan Yanysh
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Nito.AsyncEx (>= 5.1.2)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on WaitingDictionary:
| Package | Downloads |
|---|---|
|
TcpClientIo
Wrapper of TcpClient what help focus on WHAT you transfer over TCP not HOW (Async, Thread-safe, TcpSerialization with attribute schema, Big/Little endian, Cancellation support) |
|
|
StealthSharp.Network
This library contains a wrapper for Stealth |
|
|
StealthSharp
This library contains a wrapper for Stealth |
GitHub repositories
This package is not used by any popular GitHub repositories.