A comprehensive .NET framework for implementing Leader Election and Distributed Semaphores patterns with support for multiple providers including Azure Blob Storage, SQL Server, PostgreSQL, Redis, File System, In-Memory, Consul, and ZooKeeper.
- .NET 8.0 or .NET 10.0
- Supported platforms: Windows, Linux, macOS
-
Distributed Semaphores are introduced as a new coordination primitive alongside Leader Election. This is an additive feature and does not change existing leader-election APIs. See Distributed Semaphores.
-
Consul:
SessionLockDelayvalidation tightened. The permitted maximum forConsulLeaderElectionOptions.SessionLockDelaychanged from 60 minutes to 60 seconds, matching Consul's own upper bound for session lock-delay. A value greater than 60 seconds now throws anArgumentExceptionat startup instead of being accepted.// ❌ Throws ArgumentException at startup (when the provider is constructed) — // the maximum is now 60 seconds, was previously 60 minutes: var invalid = new ConsulLeaderElectionOptions { SessionLockDelay = TimeSpan.FromMinutes(5) }; // ✅ Valid — must be between 0 and 60 seconds (default is 15 seconds): var valid = new ConsulLeaderElectionOptions { SessionLockDelay = TimeSpan.FromSeconds(15) };
SessionLockDelayis aninit-only property, so it is set through an object initializer (or configuration binding), not by reassignment.Migration: if you set
SessionLockDelayabove 60 seconds, lower it to 60 seconds or less. Configurations using the default (15 seconds) are unaffected.
┌────────────────────────────────────────────────────────────────────────────┐
│ APPLICATIONS │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Sample App │ │ Multi-Provider │ │ Your Application │ │
│ │ │ │ Demo │ │ │ │
│ └──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────────┼───────────────────────┘ │
│ │ │
└─────────────────────────────────┼──────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ CORE FRAMEWORK │
│ │
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ILeaderElectionService │ │ ISemaphoreService │ │
│ │ • StartAsync() / StopAsync() │ │ • AcquireAsync() │ │
│ │ • IsLeader │ │ • TryAcquireAsync() │ │
│ │ • GetLeadershipChangesAsync() │ │ • GetStatusChangesAsync() │ │
│ └───────────────┬─────────────────┘ └───────────────┬─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ILeaderElectionProvider │ │ ISemaphoreProvider │ │
│ │ • TryAcquireLeadershipAsync() │ │ • TryAcquireAsync() │ │
│ │ • ReleaseLeadershipAsync() │ │ • ReleaseAsync() │ │
│ │ • UpdateHeartbeatAsync() │ │ • UpdateHeartbeatAsync() │ │
│ └───────────────┬─────────────────┘ └───────────────┬─────────────────┘ │
│ │ │ │
└──────────────────┼────────────────────────────────────┼────────────────────┘
│ │
└────────────────┬───────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ PROVIDERS │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ InMemory │ │ FileSystem │ │ SQL Server │ │ PostgreSQL │ │
│ │ │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Azure Blob │ │ Redis │ │ Consul │ │ ZooKeeper │ │
│ │ Storage │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
- Multiple Providers: Support for various storage backends
- Leader Election: First-to-acquire becomes leader strategy with automatic failover
- Distributed Semaphores: Control concurrent access with configurable slot limits
- Heartbeat Monitoring: Automatic health monitoring and failover
- Resilient Design: Handles transient and persistent failures gracefully
- Thread-Safe: All operations are thread-safe
- Configurable: Extensive configuration options for timeouts, retry policies, etc.
- Dependency Injection: Full support for .NET dependency injection
- Comprehensive Logging: Built-in logging and telemetry support
| Provider | Package | Description |
|---|---|---|
| Azure Blob Storage | MultiLock.AzureBlobStorage |
Uses Azure Blob Storage for coordination |
| SQL Server | MultiLock.SqlServer |
Uses SQL Server database for coordination |
| PostgreSQL | MultiLock.PostgreSQL |
Uses PostgreSQL database for coordination |
| Redis | MultiLock.Redis |
Uses Redis for coordination |
| File System | MultiLock.FileSystem |
Uses local file system (single machine) |
| In-Memory | MultiLock.InMemory |
In-memory provider for testing |
| Consul | MultiLock.Consul |
Uses HashiCorp Consul for coordination |
| ZooKeeper | MultiLock.ZooKeeper |
Uses Apache ZooKeeper for coordination |
# For SQL Server
dotnet add package MultiLock.SqlServer
# For Redis
dotnet add package MultiLock.Redis
# For PostgreSQL
dotnet add package MultiLock.PostgreSQL
# For In-Memory (testing)
dotnet add package MultiLock.InMemoryusing MultiLock;
using MultiLock.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Add leader election with In-Memory provider
builder.Services.AddLeaderElection<InMemoryLeaderElectionProvider>(options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});
var app = builder.Build();public class MyBackgroundService : BackgroundService
{
private readonly ILeaderElectionService _leaderElection;
private readonly ILogger<MyBackgroundService> _logger;
public MyBackgroundService(
ILeaderElectionService leaderElection,
ILogger<MyBackgroundService> logger)
{
_leaderElection = leaderElection;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Start listening to leadership changes using AsyncEnumerable
var leadershipTask = Task.Run(async () =>
{
await foreach (var change in _leaderElection.GetLeadershipChangesAsync(stoppingToken))
{
if (change.BecameLeader)
{
_logger.LogInformation("🎉 Leadership acquired!");
}
else if (change.LostLeadership)
{
_logger.LogWarning("😞 Leadership lost!");
}
}
}, stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
if (_leaderElection.IsLeader)
{
// Perform leader-only work
_logger.LogInformation("🏆 Performing leader work...");
await DoLeaderWork(stoppingToken);
}
else
{
// Perform follower work or wait
_logger.LogInformation("👥 Waiting as follower...");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
// Wait for leadership monitoring to complete
try
{
await leadershipTask;
}
catch (OperationCanceledException)
{
// Expected when stopping
}
}
private async Task DoLeaderWork(CancellationToken cancellationToken)
{
// Your leader-specific logic here
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}The leader election service provides a modern AsyncEnumerable API for consuming leadership change events:
// Subscribe to all leadership changes
await foreach (var change in leaderElection.GetLeadershipChangesAsync(cancellationToken))
{
if (change.BecameLeader)
{
Console.WriteLine("I am now the leader!");
}
else if (change.LostLeadership)
{
Console.WriteLine("I lost leadership!");
}
}// Only receive leadership acquired events
await foreach (var change in leaderElection.GetLeadershipChangesAsync(
LeadershipEventType.Acquired, cancellationToken))
{
Console.WriteLine("Leadership acquired!");
}
// Receive both acquired and lost events
await foreach (var change in leaderElection.GetLeadershipChangesAsync(
LeadershipEventType.Acquired | LeadershipEventType.Lost, cancellationToken))
{
// Handle events
}The framework provides powerful LINQ-style extension methods:
// Take only the first leadership acquisition
await foreach (var change in leaderElection.GetLeadershipChangesAsync(cancellationToken)
.TakeUntilLeader(cancellationToken))
{
Console.WriteLine("Waiting for leadership...");
}
// Process events while we are the leader
await foreach (var change in leaderElection.GetLeadershipChangesAsync(cancellationToken)
.WhileLeader(cancellationToken))
{
Console.WriteLine("Still the leader!");
}
// Execute callbacks on leadership transitions
await leaderElection.GetLeadershipChangesAsync(cancellationToken)
.OnLeadershipTransition(
onAcquired: e => Console.WriteLine("Acquired!"),
onLost: e => Console.WriteLine("Lost!"),
cancellationToken);
// Combine multiple extension methods
await foreach (var change in leaderElection.GetLeadershipChangesAsync(cancellationToken)
.Where(e => e.BecameLeader || e.LostLeadership, cancellationToken)
.DistinctUntilChanged(cancellationToken)
.Take(5, cancellationToken))
{
// Process filtered events
}The MultiLock framework is designed to be fully thread-safe and handle concurrent operations gracefully:
- All public methods are thread-safe: You can safely call
StartAsync(),StopAsync(),IsLeaderAsync(), and other methods from multiple threads concurrently. - Internal state protection: All internal state modifications are protected by appropriate synchronization mechanisms (locks, atomic operations, etc.).
- Safe disposal: The service implements both
IDisposableandIAsyncDisposablewith proper coordination to prevent race conditions during shutdown. - Callback coordination: Timer callbacks (heartbeat and election timers) are coordinated with disposal to prevent
ObjectDisposedExceptionand ensure clean shutdown.
All provider implementations are thread-safe and support concurrent operations:
- Concurrent leadership acquisition: Multiple participants can safely attempt to acquire leadership simultaneously - only one will succeed.
- Concurrent heartbeat updates: Multiple threads can safely update heartbeats for the same participant.
- Concurrent metadata updates: Metadata can be safely updated from multiple threads.
- Concurrent queries: Methods like
GetCurrentLeaderAsync()andIsLeaderAsync()can be called concurrently without issues.
The framework has been extensively tested for various concurrent scenarios:
When multiple participants attempt to acquire leadership simultaneously:
// Safe to run from multiple instances/threads
var tasks = Enumerable.Range(0, 10).Select(async i =>
{
var service = serviceProvider.GetRequiredService<ILeaderElectionService>();
await service.StartAsync();
// Only one will become leader
}).ToArray();
await Task.WhenAll(tasks);Exactly one participant will become the leader. All others will fail gracefully and continue monitoring for leadership opportunities.
The framework handles rapid leadership transitions without race conditions:
// Safe to rapidly acquire and release leadership
for (int i = 0; i < 100; i++)
{
await service.StartAsync();
await Task.Delay(10);
await service.StopAsync();
}Each cycle completes cleanly without resource leaks or state corruption.
Multiple threads can safely update heartbeats:
// Safe concurrent heartbeat updates
var tasks = Enumerable.Range(0, 50).Select(async i =>
{
await provider.UpdateHeartbeatAsync("election-group", "participant-id");
}).ToArray();
await Task.WhenAll(tasks);All heartbeat updates are processed atomically. The last update wins.
When a leader fails and multiple participants compete to take over:
// Current leader releases leadership
await currentLeader.StopAsync();
// Multiple waiting participants compete
// Only one will successfully acquire leadershipClean failover without split-brain scenarios. Exactly one new leader is elected.
- Use dependency injection: Register the service as a singleton to ensure a single instance per application.
services.AddSingleton<ILeaderElectionService, LeaderElectionService>();-
Avoid manual synchronization: The framework handles all necessary synchronization internally. Don't wrap calls in your own locks.
-
Use async disposal: When shutting down, prefer
DisposeAsync()overDispose()for providers that support it (Consul, ZooKeeper).
await using var provider = new ConsulLeaderElectionProvider(options, logger);
// Provider will be properly disposed asynchronously- Handle cancellation properly: Always pass
CancellationTokento async methods and handleOperationCanceledException.
try
{
await service.StartAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Clean shutdown requested
}- Monitor leadership changes: Use the
GetLeadershipChangesAsync()API to react to leadership transitions rather than polling.
await foreach (var change in service.GetLeadershipChangesAsync(cancellationToken))
{
if (change.BecameLeader)
{
// Start leader-only work
}
else if (change.LostLeadership)
{
// Stop leader-only work
}
}- Use atomic database operations (INSERT ... ON CONFLICT, MERGE) to prevent race conditions
- Connection pooling is handled automatically by the database client libraries
- No risk of deadlocks - all operations use optimistic concurrency control
- Uses atomic Redis commands (SET with NX and EX options)
- Connection multiplexing is handled by StackExchange.Redis
- No connection pool exhaustion under high concurrency
- Uses file locking for mutual exclusion
- Note: Only suitable for single-machine scenarios
- File locks are automatically released on process termination
- Uses Consul sessions for distributed locking
- Session TTL ensures automatic cleanup on failure
- Supports high concurrency across distributed systems
- Uses ZooKeeper ephemeral sequential nodes
- Automatic cleanup when client disconnects
- Handles network partitions gracefully
builder.Services.AddSqlServerLeaderElection(
connectionString: "Server=localhost;Database=MyApp;Trusted_Connection=true;",
options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});builder.Services.AddPostgreSQLLeaderElection(
connectionString: "Host=localhost;Database=MyApp;Username=user;Password=password;",
options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});builder.Services.AddRedisLeaderElection(
connectionString: "localhost:6379",
options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});builder.Services.AddAzureBlobStorageLeaderElection(
connectionString: "DefaultEndpointsProtocol=https;AccountName=...",
options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});builder.Services.AddConsulLeaderElection(
address: "http://localhost:8500",
options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});builder.Services.AddZooKeeperLeaderElection(
connectionString: "localhost:2181",
options =>
{
options.ElectionGroup = "my-service";
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});PostgreSQL with Custom Schema and Table:
builder.Services.AddPostgreSQLLeaderElection(options =>
{
options.ConnectionString = "Host=localhost;Database=myapp;Username=user;Password=pass";
options.TableName = "custom_leader_election";
options.SchemaName = "leader_election";
options.AutoCreateTable = true;
options.CommandTimeoutSeconds = 60;
}, leaderElectionOptions =>
{
leaderElectionOptions.ElectionGroup = "my-service";
leaderElectionOptions.ParticipantId = Environment.MachineName;
leaderElectionOptions.HeartbeatInterval = TimeSpan.FromSeconds(15);
leaderElectionOptions.HeartbeatTimeout = TimeSpan.FromSeconds(45);
leaderElectionOptions.EnableDetailedLogging = true;
});ZooKeeper with Custom Session Settings:
builder.Services.AddZooKeeperLeaderElection(options =>
{
options.ConnectionString = "zk1:2181,zk2:2181,zk3:2181";
options.RootPath = "/my-app/leader-election";
options.SessionTimeout = TimeSpan.FromSeconds(30);
options.ConnectionTimeout = TimeSpan.FromSeconds(10);
options.MaxRetries = 5;
options.RetryDelay = TimeSpan.FromSeconds(2);
options.AutoCreateRootPath = true;
}, leaderElectionOptions =>
{
leaderElectionOptions.ElectionGroup = "my-service";
leaderElectionOptions.ParticipantId = $"{Environment.MachineName}-{Environment.ProcessId}";
leaderElectionOptions.HeartbeatInterval = TimeSpan.FromSeconds(10);
leaderElectionOptions.HeartbeatTimeout = TimeSpan.FromSeconds(30);
});- Election: Multiple instances compete to become the leader
- Leadership: One instance becomes the leader and performs exclusive work
- Heartbeat: The leader sends periodic heartbeats to maintain leadership
- Failover: If the leader fails, a new election occurs automatically
The service provides an AsyncEnumerable API for observing leadership changes:
- Acquired: Emitted when the current instance becomes the leader
- Lost: Emitted when the current instance loses leadership
- Changed: Emitted on any leadership status change (including when another participant becomes leader)
Use GetLeadershipChangesAsync() to subscribe to these events and optionally filter by event type using LeadershipEventType flags.
- ElectionGroup: Logical group name for the election (multiple services can have different groups)
- ParticipantId: Unique identifier for this instance (auto-generated if not specified)
- HeartbeatInterval: How often the leader sends heartbeats
- HeartbeatTimeout: How long to wait before considering a leader dead
- ElectionInterval: How often followers attempt to become leader
- LockTimeout: Maximum time to hold a lock during election
- AutoStart: Whether to automatically start the election process
Distributed semaphores allow you to control concurrent access to a shared resource across multiple instances. Unlike leader election (which allows only one holder), semaphores allow a configurable number of concurrent holders.
- Rate Limiting: Limit concurrent API calls to an external service
- Resource Pooling: Control access to a limited pool of resources (database connections, licenses, etc.)
- Throttling: Limit concurrent processing of expensive operations
- Capacity Management: Ensure only N instances process work simultaneously
using MultiLock;
using MultiLock.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Add semaphore with In-Memory provider (max 5 concurrent holders)
builder.Services.AddSemaphore<InMemorySemaphoreProvider>(options =>
{
options.SemaphoreName = "api-rate-limiter";
options.MaxCount = 5;
options.HeartbeatInterval = TimeSpan.FromSeconds(30);
options.HeartbeatTimeout = TimeSpan.FromSeconds(90);
});
var app = builder.Build();public class RateLimitedApiClient
{
private readonly ISemaphoreService _semaphore;
private readonly HttpClient _httpClient;
public RateLimitedApiClient(ISemaphoreService semaphore, HttpClient httpClient)
{
_semaphore = semaphore;
_httpClient = httpClient;
}
public async Task<string> CallExternalApiAsync(CancellationToken cancellationToken)
{
// Block until a slot is available (or cancellationToken is triggered)
await _semaphore.WaitForSlotAsync(cancellationToken);
try
{
// We now hold a slot - make the API call
return await _httpClient.GetStringAsync("/api/data", cancellationToken);
}
finally
{
// Always release the slot, even if the call throws
await _semaphore.ReleaseAsync(cancellationToken);
}
}
public async Task<string?> TryCallExternalApiAsync(CancellationToken cancellationToken)
{
// Try to acquire without blocking; returns false if all slots are taken
bool acquired = await _semaphore.TryAcquireAsync(cancellationToken);
if (!acquired)
return null;
try
{
return await _httpClient.GetStringAsync("/api/data", cancellationToken);
}
finally
{
await _semaphore.ReleaseAsync(cancellationToken);
}
}
public async Task<string?> CallWithScopeAsync(CancellationToken cancellationToken)
{
// AcquireScopeAsync returns a disposable scope when a slot is acquired (null if full),
// so the slot is released automatically at the end of the using block - no try/finally.
await using SemaphoreAcquisition? scope = await _semaphore.AcquireScopeAsync(cancellationToken);
if (scope is null)
return null;
return await _httpClient.GetStringAsync("/api/data", cancellationToken);
}
}// Check current status synchronously via the property
SemaphoreStatus status = semaphore.CurrentStatus;
Console.WriteLine($"Holding: {status.IsHolding}");
Console.WriteLine($"Available: {status.AvailableSlots}/{status.MaxCount}");
// Or fetch the latest state including all holders asynchronously
SemaphoreInfo? info = await semaphore.GetSemaphoreInfoAsync(cancellationToken);
if (info != null)
Console.WriteLine($"[Async] Active holders: {info.CurrentCount}/{info.MaxCount}");
// Subscribe to status changes
await foreach (var change in semaphore.GetStatusChangesAsync(cancellationToken))
{
if (change.AcquiredSlot)
{
Console.WriteLine("Acquired a semaphore slot!");
}
else if (change.LostSlot)
{
Console.WriteLine("Lost semaphore slot!");
}
}All providers support semaphores with the same API:
// PostgreSQL
builder.Services.AddPostgreSqlSemaphore(
connectionString: "Host=localhost;Database=MyApp;...",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });
// Redis
builder.Services.AddRedisSemaphore(
connectionString: "localhost:6379",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });
// SQL Server
builder.Services.AddSqlServerSemaphore(
connectionString: "Server=localhost;Database=MyApp;...",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });
// Azure Blob Storage
builder.Services.AddAzureBlobStorageSemaphore(
connectionString: "DefaultEndpointsProtocol=https;...",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });
// Consul
builder.Services.AddConsulSemaphore(
address: "http://localhost:8500",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });
// ZooKeeper
builder.Services.AddZooKeeperSemaphore(
connectionString: "localhost:2181",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });
// File System
builder.Services.AddFileSystemSemaphore(
directoryPath: "/var/locks",
options => { options.SemaphoreName = "my-semaphore"; options.MaxCount = 10; });| Option | Description | Default |
|---|---|---|
SemaphoreName |
Unique name for the semaphore | Required |
MaxCount |
Maximum concurrent holders | Required |
HolderId |
Unique identifier for this holder | Auto-generated |
HeartbeatInterval |
How often to send heartbeats | 10 seconds |
HeartbeatTimeout |
Time before a holder is considered dead | 30 seconds |
AcquisitionInterval |
How often to retry acquiring a slot | 5 seconds |
MaxRetryAttempts |
Retry attempts for transient failures | 3 |
RetryBaseDelay |
Base delay between retries | 100ms |
RetryMaxDelay |
Maximum delay between retries | 5 seconds |
AutoStart |
Start acquiring on service start | true |
EnableDetailedLogging |
Enable verbose logging | false |
| Feature | Leader Election | Semaphore |
|---|---|---|
| Concurrent holders | 1 (exclusive) | N (configurable) |
| Use case | Single leader tasks | Rate limiting, pooling |
| Failover | Automatic re-election | Slot becomes available |
| API | IsLeader property |
IsHolding property |
Check out the sample applications:
- Basic Sample:
samples/MultiLock.Sample/- Single provider demonstration - Multi-Provider Demo:
samples/MultiLock.MultiProvider/- Multiple instances competing - Semaphore Sample:
samples/MultiLock.SemaphoreSample/- Rate-limited workers coordinated by a distributed semaphore
# Run the basic sample with different providers
cd samples/MultiLock.Sample
dotnet run inmemory
dotnet run filesystem
dotnet run sqlserver "Server=localhost;Database=Test;Trusted_Connection=true;"
dotnet run postgresql "Host=localhost;Database=leaderelection;Username=user;Password=pass"
dotnet run redis "localhost:6379"
dotnet run consul "http://localhost:8500"
dotnet run zookeeper "localhost:2181"
# Run the multi-provider demo
cd samples/MultiLock.MultiProvider
dotnet run
# Run the semaphore sample: dotnet run [provider] [maxConcurrent]
cd samples/MultiLock.SemaphoreSample
dotnet run inmemory 3The project includes comprehensive unit tests and integration tests. Integration tests use Docker containers to test against real services.
# Run unit tests
dotnet test tests/MultiLock.Tests/
# Run integration tests (requires Docker)
docker-compose up -d
dotnet test tests/MultiLock.IntegrationTests/
docker-compose downThe included docker-compose.yml file provides all the necessary services for testing:
- PostgreSQL (port 5432) - For PostgreSQL provider testing
- ZooKeeper (port 2181) - For ZooKeeper provider testing
- Redis (port 6379) - For Redis provider testing
- Consul (port 8500) - For Consul provider testing
- Azurite (ports 10000-10002) - For Azure Blob Storage provider testing
- SQL Server (port 1433) - For SQL Server provider testing
# Start all services
docker-compose up -d
# Check service health
docker-compose ps
# View logs for a specific service
docker-compose logs postgres
docker-compose logs zookeeper
# Stop all services
docker-compose down
# Stop and remove volumes (clean slate)
docker-compose down -vThe framework works seamlessly in containerized environments. See docker-compose.yml for examples of running with various backing services.
Contributions are welcome! Please see our contributing guidelines and code of conduct.
This project is licensed under the MIT License - see the LICENSE file for details.