From 4be255d6f67ebe1671a54202df964310582a1aef Mon Sep 17 00:00:00 2001
From: chunty <6099589+chunty@users.noreply.github.com>
Date: Fri, 1 May 2026 23:15:58 +0100
Subject: [PATCH 1/3] Add TaskTurnstileBuilder, SqlServer table initializer,
and Directory.Build.props
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
Directory.Build.props | 9 +++
.../SqlServerTableInitializerHostedService.cs | 60 +++++++++++++++++
.../TaskTurnstile.SqlServer.csproj | 4 --
.../TaskTurnstileBuilder.cs | 65 +++++++++++++++++++
4 files changed, 134 insertions(+), 4 deletions(-)
create mode 100644 Directory.Build.props
create mode 100644 TaskTurnstile.SqlServer/SqlServerTableInitializerHostedService.cs
create mode 100644 TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..b939ffc
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+
+ 9.0.15
+ 10.0.7
+
+
+
diff --git a/TaskTurnstile.SqlServer/SqlServerTableInitializerHostedService.cs b/TaskTurnstile.SqlServer/SqlServerTableInitializerHostedService.cs
new file mode 100644
index 0000000..36414e9
--- /dev/null
+++ b/TaskTurnstile.SqlServer/SqlServerTableInitializerHostedService.cs
@@ -0,0 +1,60 @@
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Hosting;
+using System.Data;
+
+namespace TaskTurnstile.SqlServer;
+
+internal sealed class SqlServerTableInitializerHostedService(
+ string connectionString,
+ string schemaName,
+ string tableName) : IHostedService
+{
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var quotedTable = $"[{schemaName.Replace("]", "]]")}].[{tableName.Replace("]", "]]")}]";
+ var escapedSchema = schemaName.Replace("'", "''");
+ var escapedTable = tableName.Replace("'", "''");
+
+ var tableInfoSql =
+ $"SELECT 1 FROM INFORMATION_SCHEMA.TABLES " +
+ $"WHERE TABLE_SCHEMA = '{escapedSchema}' AND TABLE_NAME = '{escapedTable}'";
+
+ var createTableSql =
+ $"CREATE TABLE {quotedTable}(" +
+ "Id nvarchar(449) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, " +
+ "Value varbinary(MAX) NOT NULL, " +
+ "ExpiresAtTime datetimeoffset NOT NULL, " +
+ "SlidingExpirationInSeconds bigint NULL, " +
+ "AbsoluteExpiration datetimeoffset NULL, " +
+ "PRIMARY KEY (Id))";
+
+ var createIndexSql =
+ $"CREATE NONCLUSTERED INDEX Index_ExpiresAtTime ON {quotedTable}(ExpiresAtTime)";
+
+ await using var connection = new SqlConnection(connectionString);
+ await connection.OpenAsync(cancellationToken);
+
+ await using var checkCommand = new SqlCommand(tableInfoSql, connection);
+ await using var reader = await checkCommand.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken);
+ var exists = await reader.ReadAsync(cancellationToken);
+ await reader.CloseAsync();
+
+ if (exists)
+ return;
+
+ await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync(cancellationToken);
+ try
+ {
+ await new SqlCommand(createTableSql, connection, transaction).ExecuteNonQueryAsync(cancellationToken);
+ await new SqlCommand(createIndexSql, connection, transaction).ExecuteNonQueryAsync(cancellationToken);
+ await transaction.CommitAsync(cancellationToken);
+ }
+ catch
+ {
+ await transaction.RollbackAsync(cancellationToken);
+ throw;
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj b/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj
index 5f4335d..864226d 100644
--- a/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj
+++ b/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj
@@ -20,10 +20,6 @@
-
-
-
-
diff --git a/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs b/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs
new file mode 100644
index 0000000..7b21c90
--- /dev/null
+++ b/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs
@@ -0,0 +1,65 @@
+using TaskTurnstile.Stores;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace TaskTurnstile.DependencyInjection;
+
+public sealed class TaskTurnstileBuilder(IServiceCollection services)
+{
+ public IServiceCollection Services { get; } = services;
+
+ ///
+ /// Uses the app's registered as the backing store.
+ /// The app must register a distributed cache (e.g. AddStackExchangeRedisCache) before building the container.
+ /// Task keys are prefixed with (default "cm:") to avoid collisions.
+ ///
+ public TaskTurnstileBuilder AddDistributedStore()
+ {
+ ReplaceStore(services => services.AddSingleton(sp =>
+ {
+ var opts = sp.GetRequiredService();
+ return new DistributedCacheTaskStateStore(
+ sp.GetRequiredService(),
+ opts.KeyPrefix);
+ }));
+ return this;
+ }
+
+ ///
+ /// Uses a dedicated instance created by the provided factory,
+ /// completely isolated from the app's own cache.
+ /// Task keys are prefixed with (default "cm:").
+ ///
+ public TaskTurnstileBuilder AddDistributedStore(Func cacheFactory)
+ {
+ ReplaceStore(services => services.AddSingleton(sp =>
+ {
+ var opts = sp.GetRequiredService();
+ return new DistributedCacheTaskStateStore(cacheFactory(sp), opts.KeyPrefix);
+ }));
+ return this;
+ }
+
+ /// Replaces the backing store with a custom implementation.
+ public TaskTurnstileBuilder UseTaskStateStore() where T : class, ITaskStateStore
+ {
+ ReplaceStore(services => services.AddSingleton());
+ return this;
+ }
+
+ /// Replaces the backing store with a custom instance created by the provided factory.
+ public TaskTurnstileBuilder UseTaskStateStore(Func factory)
+ {
+ ReplaceStore(services => services.AddSingleton(factory));
+ return this;
+ }
+
+ private void ReplaceStore(Action register)
+ {
+ var existing = Services.FirstOrDefault(d => d.ServiceType == typeof(ITaskStateStore));
+ if (existing is not null)
+ Services.Remove(existing);
+ register(Services);
+ }
+}
From 8a7f8fbe2703146afddfc2f18e82ffebe16def99 Mon Sep 17 00:00:00 2001
From: chunty <6099589+chunty@users.noreply.github.com>
Date: Fri, 1 May 2026 23:30:14 +0100
Subject: [PATCH 2/3] Fix build: restore ProjectReference in
TaskTurnstile.SqlServer
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj b/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj
index 864226d..5f4335d 100644
--- a/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj
+++ b/TaskTurnstile.SqlServer/TaskTurnstile.SqlServer.csproj
@@ -20,6 +20,10 @@
+
+
+
+
From 153db34d65eb92413113569e2cc0a193d5315e4d Mon Sep 17 00:00:00 2001
From: chunty <6099589+chunty@users.noreply.github.com>
Date: Fri, 1 May 2026 23:31:05 +0100
Subject: [PATCH 3/3] Remove old ConcurrencyManager/TaskControlTower builder
files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...s.cs => TaskTurnstileBuilderExtensions.cs} | 0
...s.cs => TaskTurnstileBuilderExtensions.cs} | 0
.../TaskControlTowerBuilder.cs | 65 -------------------
.../TaskTurnstileBuilder.cs | 1 -
4 files changed, 66 deletions(-)
rename TaskTurnstile.Redis/DependencyInjection/{ConcurrencyManagerBuilderExtensions.cs => TaskTurnstileBuilderExtensions.cs} (100%)
rename TaskTurnstile.SqlServer/DependencyInjection/{ConcurrencyManagerBuilderExtensions.cs => TaskTurnstileBuilderExtensions.cs} (100%)
delete mode 100644 TaskTurnstile/DependencyInjection/TaskControlTowerBuilder.cs
diff --git a/TaskTurnstile.Redis/DependencyInjection/ConcurrencyManagerBuilderExtensions.cs b/TaskTurnstile.Redis/DependencyInjection/TaskTurnstileBuilderExtensions.cs
similarity index 100%
rename from TaskTurnstile.Redis/DependencyInjection/ConcurrencyManagerBuilderExtensions.cs
rename to TaskTurnstile.Redis/DependencyInjection/TaskTurnstileBuilderExtensions.cs
diff --git a/TaskTurnstile.SqlServer/DependencyInjection/ConcurrencyManagerBuilderExtensions.cs b/TaskTurnstile.SqlServer/DependencyInjection/TaskTurnstileBuilderExtensions.cs
similarity index 100%
rename from TaskTurnstile.SqlServer/DependencyInjection/ConcurrencyManagerBuilderExtensions.cs
rename to TaskTurnstile.SqlServer/DependencyInjection/TaskTurnstileBuilderExtensions.cs
diff --git a/TaskTurnstile/DependencyInjection/TaskControlTowerBuilder.cs b/TaskTurnstile/DependencyInjection/TaskControlTowerBuilder.cs
deleted file mode 100644
index 7b21c90..0000000
--- a/TaskTurnstile/DependencyInjection/TaskControlTowerBuilder.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using TaskTurnstile.Stores;
-using Microsoft.Extensions.Caching.Distributed;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-
-namespace TaskTurnstile.DependencyInjection;
-
-public sealed class TaskTurnstileBuilder(IServiceCollection services)
-{
- public IServiceCollection Services { get; } = services;
-
- ///
- /// Uses the app's registered as the backing store.
- /// The app must register a distributed cache (e.g. AddStackExchangeRedisCache) before building the container.
- /// Task keys are prefixed with (default "cm:") to avoid collisions.
- ///
- public TaskTurnstileBuilder AddDistributedStore()
- {
- ReplaceStore(services => services.AddSingleton(sp =>
- {
- var opts = sp.GetRequiredService();
- return new DistributedCacheTaskStateStore(
- sp.GetRequiredService(),
- opts.KeyPrefix);
- }));
- return this;
- }
-
- ///
- /// Uses a dedicated instance created by the provided factory,
- /// completely isolated from the app's own cache.
- /// Task keys are prefixed with (default "cm:").
- ///
- public TaskTurnstileBuilder AddDistributedStore(Func cacheFactory)
- {
- ReplaceStore(services => services.AddSingleton(sp =>
- {
- var opts = sp.GetRequiredService();
- return new DistributedCacheTaskStateStore(cacheFactory(sp), opts.KeyPrefix);
- }));
- return this;
- }
-
- /// Replaces the backing store with a custom implementation.
- public TaskTurnstileBuilder UseTaskStateStore() where T : class, ITaskStateStore
- {
- ReplaceStore(services => services.AddSingleton());
- return this;
- }
-
- /// Replaces the backing store with a custom instance created by the provided factory.
- public TaskTurnstileBuilder UseTaskStateStore(Func factory)
- {
- ReplaceStore(services => services.AddSingleton(factory));
- return this;
- }
-
- private void ReplaceStore(Action register)
- {
- var existing = Services.FirstOrDefault(d => d.ServiceType == typeof(ITaskStateStore));
- if (existing is not null)
- Services.Remove(existing);
- register(Services);
- }
-}
diff --git a/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs b/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs
index 7b21c90..9aaf794 100644
--- a/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs
+++ b/TaskTurnstile/DependencyInjection/TaskTurnstileBuilder.cs
@@ -1,7 +1,6 @@
using TaskTurnstile.Stores;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
namespace TaskTurnstile.DependencyInjection;