AnoiKeyedLock 1.1.0

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

AnoiKeyedLock

A high-performance, low-allocation keyed lock implementation for .NET that ensures exclusive access per string key with automatic resource cleanup.

Features

✅ High Performance: Lock-free reference counting with minimal contention
✅ Low Allocations: Struct-based releaser avoids heap allocations
✅ Automatic Cleanup: Keys and semaphores are automatically removed when no longer needed
✅ Thread-Safe: Fully thread-safe for concurrent access
✅ Flexible API: Support for sync/async, timeouts, and cancellation tokens
✅ String Keys: Optimized for string-based locking with optional case-insensitive comparison
✅ Diagnostics: Built-in IsLocked() and GetActiveKeys() methods for monitoring
✅ IAsyncDisposable: Support for await using syntax on .NET 8+
✅ Multi-targeting: Supports .NET Standard 2.1, .NET 8, .NET 9, and .NET 10

Installation

Simply include the KeyedLock.cs file in your project, or build and reference the assembly.

Quick Start

using AnoiKeyedLock;

var keyedLock = new KeyedLock();

// Basic usage
using (var releaser = keyedLock.Lock("myKey"))
{
    // Only one thread can execute this block for "myKey" at a time
    DoWork();
}

// Async usage
using (var releaser = await keyedLock.LockAsync("myKey"))
{
    await DoWorkAsync();
}

// With timeout
if (keyedLock.TryLock("myKey", TimeSpan.FromSeconds(5), out var releaser))
{
    using (releaser)
    {
        DoWork();
    }
}

Dependency Injection

AnoiKeyedLock provides built-in support for dependency injection through the AddKeyedLock() extension method.

Setup

First, add the required NuGet package to your project:

dotnet add package Microsoft.Extensions.DependencyInjection

Then register KeyedLock in your DI container:

using AnoiKeyedLock;
using Microsoft.Extensions.DependencyInjection;

// In your Startup.cs or Program.cs
services.AddKeyedLock();

// Or with custom string comparer (e.g., case-insensitive)
services.AddKeyedLock(StringComparer.OrdinalIgnoreCase);

Usage with DI

Once registered, inject IKeyedLock into your services:

public class MyService
{
    private readonly IKeyedLock _keyedLock;

    public MyService(IKeyedLock keyedLock)
    {
        _keyedLock = keyedLock;
    }

    public async Task ProcessAsync(string id)
    {
        using (var releaser = await _keyedLock.LockAsync(id))
        {
            // Your synchronized code here
            await DoWorkAsync(id);
        }
    }
}

ASP.NET Core Example

// Program.cs or Startup.cs
var builder = WebApplication.CreateBuilder(args);

// Register KeyedLock as a singleton
builder.Services.AddKeyedLock();

// Register your services
builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

// Controller usage
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly IKeyedLock _keyedLock;

    public UserController(IKeyedLock keyedLock)
    {
        _keyedLock = keyedLock;
    }

    [HttpPost("{userId}/process")]
    public async Task<IActionResult> ProcessUser(string userId)
    {
        using (var releaser = await _keyedLock.LockAsync(userId))
        {
            // Ensure only one request processes this user at a time
            await ProcessUserDataAsync(userId);
            return Ok();
        }
    }
}

Lifetime

The KeyedLock is registered as a singleton by default, which is the recommended approach to ensure all parts of your application share the same lock instance for a given key.

API Reference

Constructor

// Default (ordinal) string comparison
var keyedLock = new KeyedLock();

// Case-insensitive comparison
var keyedLock = new KeyedLock(StringComparer.OrdinalIgnoreCase);

// Custom concurrency settings (for high-throughput scenarios)
var keyedLock = new KeyedLock(
    concurrencyLevel: Environment.ProcessorCount * 2,
    initialCapacity: 100,
    comparer: StringComparer.Ordinal);

Synchronous Methods

Lock(string key)

Acquires a lock for the specified key. Blocks indefinitely until the lock is acquired.

KeyedLockReleaser Lock(string key)
TryLock(string key, TimeSpan timeout, out KeyedLockReleaser releaser)

Tries to acquire a lock within the specified timeout.

bool TryLock(string key, TimeSpan timeout, out KeyedLockReleaser releaser)
bool TryLock(string key, int millisecondsTimeout, out KeyedLockReleaser releaser)
TryLock(string key, CancellationToken cancellationToken, out KeyedLockReleaser releaser)

Tries to acquire a lock with cancellation support.

bool TryLock(string key, CancellationToken cancellationToken, out KeyedLockReleaser releaser)

Asynchronous Methods

LockAsync(string key, CancellationToken cancellationToken = default)

Asynchronously acquires a lock for the specified key.

Task<KeyedLockReleaser> LockAsync(string key, CancellationToken cancellationToken = default)
TryLockAsync(string key, TimeSpan timeout, CancellationToken cancellationToken = default)

Tries to asynchronously acquire a lock within the specified timeout.

Task<(bool success, KeyedLockReleaser releaser)> TryLockAsync(
    string key, 
    TimeSpan timeout, 
    CancellationToken cancellationToken = default)

Task<(bool success, KeyedLockReleaser releaser)> TryLockAsync(
    string key, 
    int millisecondsTimeout, 
    CancellationToken cancellationToken = default)

Properties and Diagnostic Methods

Count

Gets the current number of keys being tracked.

int Count { get; }
IsLocked(string key)

Checks if a lock is currently held for the specified key. Useful for diagnostics.

bool IsLocked(string key)

⚠️ Note: This is a point-in-time check. The lock state may change immediately after this method returns. Do not use for synchronization decisions.

GetActiveKeys()

Gets a snapshot of all keys that currently have active locks. Useful for monitoring and debugging.

string[] GetActiveKeys()

IAsyncDisposable Support (.NET 8+)

On .NET 8 and later, KeyedLockReleaser implements IAsyncDisposable, enabling the await using syntax:

await using (var releaser = await keyedLock.LockAsync("myKey"))
{
    await DoWorkAsync();
}

How It Works

Architecture

  1. Concurrent Dictionary: Stores semaphores keyed by string keys
  2. Reference Counting: Tracks how many threads are waiting for or holding each lock
  3. Automatic Cleanup: When the reference count reaches zero, the semaphore is disposed and removed
  4. Struct-based Releaser: The KeyedLockReleaser is a struct to avoid heap allocations

Thread Safety

  • Multiple threads can acquire locks on different keys simultaneously without blocking
  • Multiple threads waiting on the same key will queue in FIFO order
  • Reference counting uses lock-free atomic operations (Interlocked)
  • Dictionary operations use ConcurrentDictionary for thread-safe access

Performance Characteristics

Operation Complexity Allocations
Lock acquisition O(1) amortized ~0 (struct releaser)
Lock release O(1) amortized 0
Key cleanup O(1) amortized 0

Use Cases

Prevent Duplicate API Calls

public class ApiClient
{
    private readonly KeyedLock _lock = new KeyedLock();

    public async Task<User> GetUserAsync(string userId)
    {
        using (var releaser = await _lock.LockAsync(userId))
        {
            return await _httpClient.GetFromJsonAsync<User>($"/users/{userId}");
        }
    }
}

File Processing Coordination

public class FileProcessor
{
    private readonly KeyedLock _lock = new KeyedLock();

    public async Task ProcessFileAsync(string filePath)
    {
        using (var releaser = await _lock.LockAsync(filePath))
        {
            // Ensure only one thread processes this file at a time
            await ProcessFileInternalAsync(filePath);
        }
    }
}

Database Connection Management

public class ConnectionManager
{
    private readonly KeyedLock _lock = new KeyedLock();

    public async Task<T> ExecuteAsync<T>(string connectionString, Func<DbConnection, Task<T>> operation)
    {
        using (var releaser = await _lock.LockAsync(connectionString))
        {
            using (var connection = new SqlConnection(connectionString))
            {
                await connection.OpenAsync();
                return await operation(connection);
            }
        }
    }
}

Rate Limiting

public class RateLimiter
{
    private readonly KeyedLock _lock = new KeyedLock();

    public async Task<bool> TryExecuteAsync(string clientId, Func<Task> action, TimeSpan minInterval)
    {
        var result = await _lock.TryLockAsync(clientId, TimeSpan.FromMilliseconds(1));
        if (result.success)
        {
            using (result.releaser)
            {
                await action();
                await Task.Delay(minInterval); // Enforce minimum interval
                return true;
            }
        }
        return false;
    }
}

Best Practices

  1. ✅ Always use using statements to ensure locks are released
  2. ✅ Keep critical sections short to minimize contention
  3. ✅ Use async methods for I/O operations to avoid blocking threads
  4. ✅ Keys cannot be null or whitespace - validation is performed automatically
  5. ✅ Handle timeouts gracefully - always have a fallback strategy
  6. ❌ Avoid nested locks on the same key (not reentrant)
  7. ❌ Don't hold locks across long-running operations without timeouts

Requirements

  • .NET Standard 2.1 or higher, or .NET 8/9/10
  • C# 7.3 or higher

Compatible With

  • .NET Standard 2.1+
  • .NET Core 3.0+
  • .NET 5, 6, 7, 8, 9, 10+
  • Xamarin (iOS 12.16+, Android 10.0+)
  • Unity 2021.2+

Examples

See Examples.cs for comprehensive usage examples including:

  • Basic synchronous and asynchronous locking
  • Timeout handling
  • Cancellation support
  • Real-world scenarios (file processing, API calls, etc.)
  • Performance testing

License

MIT

Contributing

Contributions are welcome! Please feel free to submit pull requests or open issues.

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

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 178 1/24/2026
1.0.0 148 1/20/2026

v1.1.0:
- Added multi-targeting support for .NET 8, .NET 9, and .NET 10
- Added IAsyncDisposable support for 'await using' syntax (.NET 8+)
- Added IsLocked() method for diagnostics
- Added GetActiveKeys() method for monitoring
- Added constructor overload with concurrencyLevel and initialCapacity parameters
- Added DI extension method for concurrency settings
- Improved robustness with ObjectDisposedException handling
- Performance improvements with readonly struct fields