TaskTurnstile 1.0.0
See the version list below for details.
dotnet add package TaskTurnstile --version 1.0.0
NuGet\Install-Package TaskTurnstile -Version 1.0.0
<PackageReference Include="TaskTurnstile" Version="1.0.0" />
<PackageVersion Include="TaskTurnstile" Version="1.0.0" />
<PackageReference Include="TaskTurnstile" />
paket add TaskTurnstile --version 1.0.0
#r "nuget: TaskTurnstile, 1.0.0"
#:package TaskTurnstile@1.0.0
#addin nuget:?package=TaskTurnstile&version=1.0.0
#tool nuget:?package=TaskTurnstile&version=1.0.0
Task Turnstile
A thread-safe named task lifecycle manager for .NET. Prevents duplicate background job execution across threads and — optionally — across multiple application instances via a distributed backing store.
Think of it like a turnstile. Every job that wants to run must push through first. Only one can hold the bar at a time — others wait their turn or are sent away. When the job is done, the bar rotates and the next one can step through.
Why?
Scheduled jobs (Coravel, Hangfire, Quartz) fire on a timer. If the previous run hasn't finished, you don't want a second one to start. TaskTurnstile gives you a named gate:
if (!await _concurrencyManager.CanStartAsync("import-job"))
return; // already running, skip this tick
Unlike a simple lock, the state can survive app restarts (via Redis or SQL Server) and be shared across multiple instances of your app.
Setup
In-memory (single instance, no persistence)
builder.Services.AddTaskTurnstile();
The default store is a private in-memory cache — isolated from your app's own IMemoryCache and requiring zero configuration.
Redis
builder.Services.AddTaskTurnstile()
.AddRedisStore(o => o.Configuration = "localhost:6379");
This creates a dedicated Redis connection for TaskTurnstile, independent of any other Redis cache your app may be using. Configure it with any connection string — it can point at the same Redis instance as your app or a completely separate one.
If you'd prefer TaskTurnstile to share your app's existing IDistributedCache instead, use AddDistributedStore() (see below).
SQL Server
builder.Services.AddTaskTurnstile()
.AddSqlServerStore(o =>
{
o.ConnectionString = "Server=.;Database=MyApp;...";
o.TableName = "ActiveTasks";
o.SchemaName = "dbo";
});
Note: The cache table must be created before first use:
dotnet sql-cache create "Server=.;Database=MyApp;..." dbo ActiveTasks
Use the app's existing IDistributedCache
If you've already registered a distributed cache (e.g. AddStackExchangeRedisCache) and want TaskTurnstile to share it:
builder.Services.AddTaskTurnstile()
.AddDistributedStore();
Task keys are prefixed with KeyPrefix (default "cm:") to avoid collisions with your own cache entries. Override it in options if needed.
Options
builder.Services.AddTaskTurnstile(o =>
{
// Maximum time a task can run before it's considered stale.
// Prevents tasks from being stuck forever if TryStopAsync is never called (e.g. app crash).
o.DefaultMaxRuntime = TimeSpan.FromHours(2);
// Remove all running task records on startup.
// Useful for clearing state left behind by a previous crashed process.
o.CleanupOnStartup = true;
// Prefix applied to all keys in the backing store.
// Change this if "cm:" collides with your own cache keys (only relevant when using AddDistributedStore()).
o.KeyPrefix = "myapp:tasks:";
});
API
Inject ITaskStateManager into your class:
public class ImportJob(ITaskStateManager manager)
Check if a task can start
bool canStart = await manager.CanStartAsync("import-job");
Returns true if the task is not currently running (or its maxRuntime has expired).
Run with automatic start/stop (recommended)
// Returns false immediately if already running; true after work completes.
bool ran = await manager.TryRunAsync("import-job", async ct =>
{
await DoImportAsync(ct);
});
// With a return value:
var result = await manager.TryRunAsync("import-job", async ct =>
{
return await DoImportAsync(ct);
});
if (result.Started)
Console.WriteLine($"Imported {result.Value} records");
Wait for a task to be free, then run
// Waits until "import-job" is free, then starts and runs the work.
await manager.RunAsync("import-job", async ct =>
{
await DoImportAsync(ct);
});
Manual start/stop
if (!await manager.StartAsync("import-job"))
return; // already running
try
{
await DoImportAsync(cancellationToken);
}
finally
{
await manager.TryStopAsync("import-job");
}
Per-task runtime override
All methods accept an optional maxRuntime to override the global default:
await manager.TryRunAsync("long-job", DoWorkAsync, maxRuntime: TimeSpan.FromHours(4));
Real-world patterns
Coravel invocable (skip if already running)
public class ImportInvocable(ITaskStateManager manager) : IInvocable
{
public async Task Invoke()
{
await manager.TryRunAsync("import", async ct =>
{
await DoImportAsync(ct);
});
}
}
BackgroundService (wait for previous run to finish)
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await manager.RunAsync("sync", async ct =>
{
await DoSyncAsync(ct);
}, cancellationToken: stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
Custom store
Implement ITaskStateStore to back the manager with anything — a file, a database, an API:
public class MyCustomStore : ITaskStateStore
{
public Task<bool> IsRunningAsync(string taskName, CancellationToken ct = default) { ... }
public Task<bool> IsExpiredAsync(string taskName, CancellationToken ct = default) { ... }
public Task SetRunningAsync(string taskName, TimeSpan? maxRuntime = null, CancellationToken ct = default) { ... }
public Task SetStoppedAsync(string taskName, CancellationToken ct = default) { ... }
public Task CleanupAsync(CancellationToken ct = default) { ... }
}
Register it:
// Let DI create it:
builder.Services.AddTaskTurnstile()
.UseTaskStateStore<MyCustomStore>();
// Or use a factory:
builder.Services.AddTaskTurnstile()
.UseTaskStateStore(sp => new MyCustomStore("/var/run/locks"));
See samples/FileStore/FileTaskStateStore.cs for a complete file-based example.
| 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
- AsyncKeyedLock (>= 8.0.2)
- Microsoft.Extensions.Caching.Memory (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.7)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on TaskTurnstile:
| Package | Downloads |
|---|---|
|
TaskTurnstile.Redis
Redis backing store for TaskTurnstile via StackExchange.Redis and IDistributedCache. Supports dedicated Redis instances independent of the app's own cache. |
|
|
TaskTurnstile.SqlServer
SQL Server backing store for TaskTurnstile via IDistributedCache. Supports dedicated SQL Server instances independent of the app's own cache. The cache table is created automatically on first startup. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.3 | 108 | 5/2/2026 |
| 1.0.1-alpha.0.8 | 56 | 5/2/2026 |
| 1.0.1-alpha.0.2 | 51 | 5/2/2026 |
| 1.0.0 | 89 | 5/1/2026 |
| 0.0.0-alpha.0.15 | 43 | 5/1/2026 |