TaskTurnstile 1.0.0

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

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).

// 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 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 (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