Rystem.Concurrency 10.0.7

dotnet add package Rystem.Concurrency --version 10.0.7
                    
NuGet\Install-Package Rystem.Concurrency -Version 10.0.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Rystem.Concurrency" Version="10.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Rystem.Concurrency" Version="10.0.7" />
                    
Directory.Packages.props
<PackageReference Include="Rystem.Concurrency" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Rystem.Concurrency --version 10.0.7
                    
#r "nuget: Rystem.Concurrency, 10.0.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Rystem.Concurrency@10.0.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Rystem.Concurrency&version=10.0.7
                    
Install as a Cake Addin
#tool nuget:?package=Rystem.Concurrency&version=10.0.7
                    
Install as a Cake Tool

What is Rystem?

Rystem.Concurrency

Rystem.Concurrency adds two small async concurrency primitives on top of the Rystem DI stack:

  • ILock for serialized async execution
  • IRaceCodition for deduplicating concurrent calls within a time window

Both 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:

  • guarding critical async sections
  • de-duplicating cache refreshes and polling work
  • coordinating lightweight background tasks
  • keeping the same API while switching from local memory to distributed locking

The public types live in System.Threading.Concurrent, so that is the namespace you usually import when consuming the package.

Resources

Installation

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.

Package Architecture

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 ILockable
  • AddLockExecutor() registers only the ILock executor
  • AddLock() wires both together for the default lock setup
  • AddRaceConditionExecutor() registers only the IRaceCodition executor
  • AddRaceCondition() wires the full in-memory race-condition stack

That split is important when you want to plug in a custom or distributed backend without changing the calling code.

Table of Contents


Async Lock

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.

Setup

services.AddLock();

This registers:

  • ILockLockExecutor
  • ILockableMemoryLock

ILock contract

public 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;
}

Behavior

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 lock
  • different keys can run in parallel
  • ExecutionTime includes both waiting time and action time because timing starts before acquisition
  • exceptions are captured in the response instead of being rethrown directly

LockResponse

public 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.


Race Condition Guard

IRaceCodition is a first-wins guard for async work.

When multiple callers hit the same key inside the guarded window:

  • the first caller executes the action
  • the later callers wait until the guard is released
  • those later callers return without executing the action

The interface name is intentionally documented as IRaceCodition because that is the current public API surface in the package.

Setup

services.AddRaceCondition();

This wires:

  • ILockableMemoryLock
  • ILockLockExecutor
  • IRaceCoditionRaceConditionExecutor

IRaceCodition contract

public 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;
}

Behavior

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:

  • default timeWindow is TimeSpan.FromMinutes(1)
  • omitted keys also collapse to string.Empty
  • the winner keeps the lock until the action finishes and the time window has elapsed, whichever is later in the in-memory flow
  • non-winning callers wait for release, then return IsExecuted = false

RaceConditionResponse

public sealed class RaceConditionResponse
{
    public bool IsExecuted { get; }
    public AggregateException? Exceptions { get; }
    public bool InException => this.Exceptions != default;
}
  • IsExecuted = true only for the winning caller
  • InException and Exceptions reflect failures from the winning execution

ILockable and Custom Backends

ILock and IRaceCodition both delegate the actual locking primitive to ILockable.

ILockable contract

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.

Built-in in-memory backend

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>();

Custom executor registration

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.


Distributed Locking with Redis

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.


Repository Examples

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 Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

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.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.7 0 3/26/2026
10.0.6 138,887 3/3/2026
10.0.5 204 2/22/2026
10.0.4 216 2/9/2026
10.0.3 147,970 1/28/2026
10.0.1 209,390 11/12/2025
9.1.3 419 9/2/2025
9.1.2 764,960 5/29/2025
9.1.1 97,971 5/2/2025
9.0.32 186,798 4/15/2025
9.0.31 5,908 4/2/2025
9.0.30 88,894 3/26/2025
9.0.29 9,079 3/18/2025
9.0.28 325 3/17/2025
9.0.27 293 3/16/2025
9.0.26 311 3/13/2025
9.0.25 52,167 3/9/2025
9.0.23 262 3/9/2025
9.0.21 781 3/6/2025
9.0.20 19,632 3/6/2025
Loading failed