GM.DistributedLock
1.0.0
dotnet add package GM.DistributedLock --version 1.0.0
NuGet\Install-Package GM.DistributedLock -Version 1.0.0
<PackageReference Include="GM.DistributedLock" Version="1.0.0" />
<PackageVersion Include="GM.DistributedLock" Version="1.0.0" />
<PackageReference Include="GM.DistributedLock" />
paket add GM.DistributedLock --version 1.0.0
#r "nuget: GM.DistributedLock, 1.0.0"
#:package GM.DistributedLock@1.0.0
#addin nuget:?package=GM.DistributedLock&version=1.0.0
#tool nuget:?package=GM.DistributedLock&version=1.0.0
<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
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 | Versions 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. |
-
net10.0
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 |