GM.DistributedLock 1.0.0

dotnet add package GM.DistributedLock --version 1.0.0
                    
NuGet\Install-Package GM.DistributedLock -Version 1.0.0
                    
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="GM.DistributedLock" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.DistributedLock" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.DistributedLock" />
                    
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 GM.DistributedLock --version 1.0.0
                    
#r "nuget: GM.DistributedLock, 1.0.0"
                    
#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 GM.DistributedLock@1.0.0
                    
#: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=GM.DistributedLock&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.DistributedLock&version=1.0.0
                    
Install as a Cake Tool

<p align="center"> <img src="https://raw.githubusercontent.com/gmetskhvarishvili/GM.DistributedLock/master/icon.png" alt="GM.DistributedLock" width="140" height="140" /> </p>

GM.DistributedLock

CI NuGet License: MIT

A small, provider-agnostic distributed lock for .NET. Depend on one interface — IDistributedLock — with TryAcquire (single shot) and Acquire (blocking with retry), each returning an IAsyncDisposable handle that releases on dispose and is owner-safe (it never releases a lock that expired and was re-acquired by someone else). Ships a single-process in-memory implementation; add a real backend with GM.DistributedLock.Redis. Targets .NET 10.

Packages

The two packages version and release together (lockstep):

Package What it gives you
GM.DistributedLock IDistributedLock, ILockHandle, and a single-process in-memory implementation (AddGMDistributedLock()) for dev and tests.
GM.DistributedLock.Redis A real cross-process lock over StackExchange.Redis — SET NX PX to acquire, owner-checked Lua to release (AddGMRedisDistributedLock()).
dotnet add package GM.DistributedLock          # in-process
dotnet add package GM.DistributedLock.Redis     # + Redis backend

Quick start

Register

using GM.DistributedLock;         // in-process
builder.Services.AddGMDistributedLock();

// or, cross-process:
using GM.DistributedLock.Redis;
builder.Services.AddGMRedisDistributedLock(o => o.ConnectionString = "localhost:6379");
// or bind from configuration: AddGMRedisDistributedLock(builder.Configuration, "Redis");

Use it

Inject IDistributedLock — the same code works with either backend:

public class PayoutJob(IDistributedLock locks)
{
    public async Task RunAsync(Guid accountId)
    {
        // Block up to 30s, retrying every 200ms, holding the lock for at most 60s.
        await using var handle = await locks.AcquireAsync(
            resource: $"payout:{accountId}",
            expiry: TimeSpan.FromSeconds(60),
            wait: TimeSpan.FromSeconds(30),
            retryInterval: TimeSpan.FromMilliseconds(200));

        // ... critical section: only one worker runs this per account ...
    }   // lock released here
}

Prefer a non-blocking attempt? Use TryAcquireAsync, which returns null when the lock is held:

await using var handle = await locks.TryAcquireAsync($"report:{id}", TimeSpan.FromMinutes(5));
if (handle is null) return; // someone else is already generating it

The interface

Task<ILockHandle?> TryAcquireAsync(string resource, TimeSpan expiry, CancellationToken ct = default);
Task<ILockHandle>  AcquireAsync(string resource, TimeSpan expiry, TimeSpan wait, TimeSpan retryInterval, CancellationToken ct = default);

AcquireAsync throws LockAcquisitionException if it can't take the lock within wait. Every lock has an expiry (TTL) so a crashed holder can't wedge the resource forever.

The Redis provider targets a single Redis endpoint (SET NX PX + owner-checked release), which is the right trade-off for the vast majority of apps. It is not a multi-node Redlock quorum.

Repository layout

GM.DistributedLock/              # IDistributedLock, ILockHandle, in-memory implementation
GM.DistributedLock.Redis/        # Redis implementation (StackExchange.Redis)
tests/GM.DistributedLock.Tests/  # xUnit tests for the in-memory lock

Building & testing

dotnet build -c Release
dotnet test  -c Release

Releasing

Versioning is automated from Conventional Commits — see CONTRIBUTING.md. Both packages share one version (Directory.Build.props) and publish together to nuget.org on each release.

License

MIT — see LICENSE.

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

Showing the top 5 NuGet packages that depend on GM.DistributedLock:

Package Downloads
GM.RealTime.Persistence

Cache-backed IConnectionRegistry for GM.RealTime: stores user↔connection and group membership in GM.Caching (Redis-ready) and guards the read-modify-write with GM.DistributedLock, so presence is correct and shared across server nodes. Register with AddGMRealTimeCacheStore().

GM.RateLimiting

Distributed rate limiting for the GM.* ecosystem. A provider-agnostic IRateLimiterService with policy-based checks (TryAcquireAsync(key, policy)) and three per-policy algorithms — fixed window, sliding window, and token bucket. Counters live in GM.Caching so limits hold across every service instance (unlike ASP.NET Core's per-process limiter), and the check-and-increment is made atomic by a swappable store (default backed by GM.DistributedLock). Composable key strategies (per-user / per-API-key / per-IP / per-endpoint), policies from appsettings or fluent code, and a 429-friendly result (limit / remaining / retry-after). One call — AddGMRateLimiting() — to wire it up.

GM.Idempotency

Idempotency-key management for the GM.* ecosystem. A provider-agnostic IIdempotencyService (IsProcessed / MarkAsProcessed / TryGetCachedResult, plus a race-safe ExecuteAsync check-and-set) backed by GM.Caching for TTL-based dedup-key storage and guarded by GM.DistributedLock so two near-simultaneous duplicates cannot both pass the "not yet processed" gate. Stores enough of the original result to replay it safely. Flexible key strategy (caller-supplied or derived/hashed) and one call — AddGMIdempotency() — to wire it up. HTTP middleware and GM.Mediator behavior ship in the GM.Idempotency.Http / .Mediator packages.

GM.HealthChecks.DistributedLock

Distributed-lock health check for GM.HealthChecks: acquires and releases a throwaway probe lock through GM.DistributedLock's IDistributedLock, verifying the lock backend (in-memory or Redis) is reachable. Register with AddGMDistributedLockCheck().

GM.DistributedLock.Redis

A Redis-backed IDistributedLock for GM.DistributedLock (StackExchange.Redis): acquire with SET NX PX and release with an owner-checked Lua script, so a lock is only ever released by its owner. Register with AddGMRedisDistributedLock().

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 249 8/2/2026