![]() |
VOOZH | about |
dotnet add package Rystem.Concurrency --version 10.0.8
NuGet\Install-Package Rystem.Concurrency -Version 10.0.8
<PackageReference Include="Rystem.Concurrency" Version="10.0.8" />
<PackageVersion Include="Rystem.Concurrency" Version="10.0.8" />Directory.Packages.props
<PackageReference Include="Rystem.Concurrency" />Project file
paket add Rystem.Concurrency --version 10.0.8
#r "nuget: Rystem.Concurrency, 10.0.8"
#:package Rystem.Concurrency@10.0.8
#addin nuget:?package=Rystem.Concurrency&version=10.0.8Install as a Cake Addin
#tool nuget:?package=Rystem.Concurrency&version=10.0.8Install as a Cake Tool
Rystem.Concurrency adds two small async concurrency primitives on top of the Rystem DI stack:
ILock for serialized async executionIRaceCodition for deduplicating concurrent calls within a time windowBoth abstractions are built on ILockable, which defaults to an in-memory implementation and can be swapped for another backend such as Redis.
The package is most useful for:
The public types live in System.Threading.Concurrent, so that is the namespace you usually import when consuming the package.
dotnet add package Rystem.Concurrency
Optional distributed backend:
dotnet add package Rystem.Concurrency.Redis
The current 10.x package targets net10.0 and builds on top of Rystem.DependencyInjection.
The package is organized in three layers.
| Layer | Purpose |
|---|---|
ILockable |
Lowest-level acquire / inspect / release abstraction |
ILock |
Serialized execution of async work for a key |
IRaceCodition |
First-wins execution with a configurable deduplication window |
The DI registrations mirror that layering:
AddInMemoryLockable() registers only the in-memory ILockableAddLockExecutor() registers only the ILock executorAddLock() wires both together for the default lock setupAddRaceConditionExecutor() registers only the IRaceCodition executorAddRaceCondition() wires the full in-memory race-condition stackThat split is important when you want to plug in a custom or distributed backend without changing the calling code.
ILock is the async equivalent of a critical section keyed by a string.
Use it when all callers for the same key must execute one after another instead of overlapping.
services.AddLock();
This registers:
ILock → LockExecutorILockable → MemoryLockpublic interface ILock
{
Task<LockResponse> ExecuteAsync(Func<Task> action, string? key = null);
}
Typical usage:
using System.Threading.Concurrent;
public sealed class InventoryService
{
private readonly ILock _lock;
public InventoryService(ILock @lock)
{
_lock = @lock;
}
public async Task UpdateAsync()
{
LockResponse response = await _lock.ExecuteAsync(
async () =>
{
await Task.Delay(15);
await SaveAsync();
},
key: "inventory");
if (response.InException)
throw response.Exceptions!;
}
private Task SaveAsync() => Task.CompletedTask;
}
The repository test in src/Extensions/Concurrency/Test/Rystem.Concurrency.UnitTest/LockTest.cs is the clearest example of the intended behavior.
It starts 100 concurrent calls against the same lock:
var locking = provider.CreateScope().ServiceProvider.GetService<ILock>();
for (int i = 0; i < 100; i++)
tasks.Add(locking!.ExecuteAsync(() => CountAsync(2)));
Because all calls share the same keyless default lock, they are serialized and the final counter is deterministic:
Assert.Equal(100 * 2, counter);
Important details from the implementation:
key: null becomes string.Empty, so omitted keys all share one common lockExecutionTime includes both waiting time and action time because timing starts before acquisitionpublic sealed class LockResponse
{
public TimeSpan ExecutionTime { get; }
public AggregateException? Exceptions { get; }
public bool InException => this.Exceptions != default;
}
Use InException as the quick status check and Exceptions when you want the captured failure details.
IRaceCodition is a first-wins guard for async work.
When multiple callers hit the same key inside the guarded window:
The interface name is intentionally documented as IRaceCodition because that is the current public API surface in the package.
services.AddRaceCondition();
This wires:
ILockable → MemoryLockILock → LockExecutorIRaceCodition → RaceConditionExecutorpublic interface IRaceCodition
{
Task<RaceConditionResponse> ExecuteAsync(
Func<Task> action,
string? key = null,
TimeSpan? timeWindow = null);
}
Typical usage:
using System.Threading.Concurrent;
public sealed class PriceCacheService
{
private readonly IRaceCodition _raceCondition;
public PriceCacheService(IRaceCodition raceCondition)
{
_raceCondition = raceCondition;
}
public async Task RefreshAsync(string productId)
{
var response = await _raceCondition.ExecuteAsync(
async () =>
{
await Task.Delay(15);
await RefreshCoreAsync(productId);
},
key: productId,
timeWindow: TimeSpan.FromSeconds(10));
if (response.InException)
throw response.Exceptions!;
if (response.IsExecuted)
{
// this caller won and executed the action
}
}
private Task RefreshCoreAsync(string productId) => Task.CompletedTask;
}
The repository test in src/Extensions/Concurrency/Test/Rystem.Concurrency.UnitTest/RaceConditionTest.cs runs 100 concurrent calls but alternates between only two keys:
for (int i = 0; i < 100; i++)
tasks.Add(raceCondition!.ExecuteAsync(
() => CountAsync(2),
(i % 2).ToString(),
TimeSpan.FromSeconds(2)));
Only the first call for key 0 and the first call for key 1 execute, so the final result is:
Assert.Equal(4, counter);
Important details from the implementation:
timeWindow is TimeSpan.FromMinutes(1)string.EmptyIsExecuted = falsepublic sealed class RaceConditionResponse
{
public bool IsExecuted { get; }
public AggregateException? Exceptions { get; }
public bool InException => this.Exceptions != default;
}
IsExecuted = true only for the winning callerInException and Exceptions reflect failures from the winning executionILock and IRaceCodition both delegate the actual locking primitive to ILockable.
public interface ILockable
{
Task<bool> AcquireAsync(string key, TimeSpan? maxWindow = null);
Task<bool> IsAcquiredAsync(string key);
Task<bool> ReleaseAsync(string key);
}
maxWindow matters mostly for backends that can encode expiration directly, such as Redis.
The built-in implementation is MemoryLock, registered through:
services.AddInMemoryLockable();
If you only want the low-level backend and plan to wire your own executors, this is the smallest registration unit.
To replace the backend entirely:
services.AddLockableIntegration<MyDistributedLockable>();
If you want to keep the lockable but swap the higher-level behavior:
services.AddLockExecutor<MyCustomLock>();
services.AddRaceConditionExecutor<MyCustomRaceCondition>();
There are also non-generic registrations for the default executors only:
services.AddLockExecutor();
services.AddRaceConditionExecutor();
Those methods register the executors but do not automatically add a lockable backend, so pair them with AddInMemoryLockable(), AddLockableIntegration<T>(), or the Redis package.
For multi-process or multi-host coordination, use Rystem.Concurrency.Redis.
That companion package exposes:
AddRedisLock(...)AddRaceConditionWithRedis(...)AddRedisLockable(...)Example:
services.AddRedisLock(options =>
{
options.ConnectionString = configuration["ConnectionString:Redis"]!;
});
The Redis-backed lock test in src/Extensions/Concurrency/Test/Rystem.Concurrency.UnitTest/RedisLockTest.cs uses the same ILock API as the in-memory version, which is exactly the point of the ILockable abstraction.
The most useful sources for this package are:
This README stays intentionally focused because Rystem.Concurrency is a small package with a layered design: one low-level lockable abstraction and two higher-level execution patterns built on top of it.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 3 NuGet packages that depend on Rystem.Concurrency:
| Package | Downloads |
|---|---|
|
Rystem.Test.XUnit
Rystem is a open-source framework to improve the System namespace in .Net |
|
|
Rystem.BackgroundJob
Rystem. |
|
|
Rystem.Concurrency.Redis
Rystem. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.8 | 6,039 | 5/13/2026 |
| 10.0.7 | 267 | 3/26/2026 |
| 10.0.6 | 433,742 | 3/3/2026 |
| 10.0.5 | 237 | 2/22/2026 |
| 10.0.4 | 245 | 2/9/2026 |
| 10.0.3 | 148,012 | 1/28/2026 |
| 10.0.1 | 209,449 | 11/12/2025 |
| 9.1.3 | 448 | 9/2/2025 |
| 9.1.2 | 764,984 | 5/29/2025 |
| 9.1.1 | 97,995 | 5/2/2025 |
| 9.0.32 | 186,833 | 4/15/2025 |
| 9.0.31 | 5,930 | 4/2/2025 |
| 9.0.30 | 88,922 | 3/26/2025 |
| 9.0.29 | 9,105 | 3/18/2025 |
| 9.0.28 | 349 | 3/17/2025 |
| 9.0.27 | 316 | 3/16/2025 |
| 9.0.26 | 338 | 3/13/2025 |
| 9.0.25 | 52,197 | 3/9/2025 |
| 9.0.23 | 286 | 3/9/2025 |
| 9.0.21 | 803 | 3/6/2025 |