From d61115300e51bc24fb9297e694c98234a91661ed Mon Sep 17 00:00:00 2001
From: Volodymyr Dombrovskyi <5788605+dombrovsky@users.noreply.github.com>
Date: Sun, 16 Aug 2026 17:07:47 -0600
Subject: [PATCH 1/2] Add extensible task scheduler middleware pipeline
---
.../LoggingTaskSchedulerExtensionsFixture.cs | 2 +-
.../LoggingTaskSchedulerExtensions.cs | 181 +++-----
...ExceptionTaskSchedulerExtensionsFixture.cs | 54 +++
...erceptionTaskSchedulerExtensionsFixture.cs | 6 +-
...askSchedulerMiddlewareExtensionsFixture.cs | 421 ++++++++++++++++++
.../TimeoutTaskSchedulerExtensionsFixture.cs | 4 +-
...OwnershipTaskSchedulerExtensionsFixture.cs | 16 +
TaskFlow.sln | 5 +-
.../AnnotatingTaskSchedulerExtensions.cs | 13 +-
.../CancelPreviousTaskSchedulerExtensions.cs | 17 +-
...ancellationScopeTaskSchedulerExtensions.cs | 19 +-
.../ExceptionTaskSchedulerExtensions.cs | 40 +-
.../InterceptionTaskSchedulerExtensions.cs | 111 ++---
.../TaskSchedulerInterceptionContext.cs | 12 +-
.../ThrottlingTaskSchedulerExtensions.cs | 14 +-
.../TimeoutTaskSchedulerExtensions.cs | 25 +-
TaskFlow/Internal/AnnotationScope.cs | 38 ++
TaskFlow/Internal/MiddlewareRegistration.cs | 15 +
TaskFlow/Internal/PipelineOperation.cs | 78 ++++
TaskFlow/Internal/TaskSchedulerPipeline.cs | 171 +++++++
.../ITaskSchedulerCompletionMiddleware.cs | 32 ++
.../ITaskSchedulerEnqueueMiddleware.cs | 26 ++
.../ITaskSchedulerExecutionMiddleware.cs | 25 ++
.../Middleware/ITaskSchedulerMiddleware.cs | 15 +
.../TaskSchedulerCompletionDelegate.cs | 20 +
.../Middleware/TaskSchedulerEnqueueContext.cs | 89 ++++
.../TaskSchedulerEnqueueDelegate.cs | 18 +
.../TaskSchedulerExecutionDelegate.cs | 17 +
.../TaskSchedulerMiddlewareExtensions.cs | 133 ++++++
.../TaskSchedulerOperationContext.cs | 83 ++++
.../TaskSchedulerOperationOutcome.cs | 72 +++
docs/customization.md | 216 ++++++++-
docs/extensions/cancellation.md | 2 +
docs/extensions/observability.md | 16 +-
docs/extensions/reliability.md | 2 +
docs/semantics-and-pitfalls.md | 12 +-
36 files changed, 1737 insertions(+), 283 deletions(-)
create mode 100644 TaskFlow.Tests/Extensions/TaskSchedulerMiddlewareExtensionsFixture.cs
create mode 100644 TaskFlow/Internal/AnnotationScope.cs
create mode 100644 TaskFlow/Internal/MiddlewareRegistration.cs
create mode 100644 TaskFlow/Internal/PipelineOperation.cs
create mode 100644 TaskFlow/Internal/TaskSchedulerPipeline.cs
create mode 100644 TaskFlow/Middleware/ITaskSchedulerCompletionMiddleware.cs
create mode 100644 TaskFlow/Middleware/ITaskSchedulerEnqueueMiddleware.cs
create mode 100644 TaskFlow/Middleware/ITaskSchedulerExecutionMiddleware.cs
create mode 100644 TaskFlow/Middleware/ITaskSchedulerMiddleware.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerCompletionDelegate.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerEnqueueContext.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerEnqueueDelegate.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerExecutionDelegate.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerMiddlewareExtensions.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerOperationContext.cs
create mode 100644 TaskFlow/Middleware/TaskSchedulerOperationOutcome.cs
diff --git a/TaskFlow.Extensions.Microsoft.Logging.Tests/LoggingTaskSchedulerExtensionsFixture.cs b/TaskFlow.Extensions.Microsoft.Logging.Tests/LoggingTaskSchedulerExtensionsFixture.cs
index 92509fc..db8805d 100644
--- a/TaskFlow.Extensions.Microsoft.Logging.Tests/LoggingTaskSchedulerExtensionsFixture.cs
+++ b/TaskFlow.Extensions.Microsoft.Logging.Tests/LoggingTaskSchedulerExtensionsFixture.cs
@@ -29,7 +29,7 @@ public async Task WithLogging_LogsFullLifecycleAtTraceByDefault()
{
_taskFlow = new TaskFlow();
var logger = new RecordingLogger(LogLevel.Trace);
- Assert.That(await _taskFlow.WithLogging(logger).WithOperationName("answer").Enqueue(() => 42), Is.EqualTo(42));
+ Assert.That(await _taskFlow.WithOperationName("answer").WithLogging(logger).Enqueue(() => 42), Is.EqualTo(42));
Assert.That(logger.Entries.Select(x => x.EventId.Id), Is.EqualTo(new[] { EnqueuedEventId, StartedEventId, SucceededEventId, FinishedEventId }));
Assert.That(logger.Entries, Has.All.Property(nameof(LogEntry.Level)).EqualTo(LogLevel.Trace));
Assert.That(logger.Entries.Select(x => x.Message), Has.All.Contains("operation 1"));
diff --git a/TaskFlow.Extensions.Microsoft.Logging/LoggingTaskSchedulerExtensions.cs b/TaskFlow.Extensions.Microsoft.Logging/LoggingTaskSchedulerExtensions.cs
index 15a129e..fa2140f 100644
--- a/TaskFlow.Extensions.Microsoft.Logging/LoggingTaskSchedulerExtensions.cs
+++ b/TaskFlow.Extensions.Microsoft.Logging/LoggingTaskSchedulerExtensions.cs
@@ -14,80 +14,76 @@ public static class LoggingTaskSchedulerExtensions
private static readonly EventId FailedEvent = new EventId(0x5446_0005, "TaskFlowOperationFailed");
private static readonly EventId FinishedEvent = new EventId(0x5446_0006, "TaskFlowOperationFinished");
- /// Wraps a scheduler with configurable structured lifecycle logging.
+ /// Registers structured enqueue and execution lifecycle logging for every scheduled operation.
/// The scheduler whose operations will be logged.
/// The logger that receives lifecycle events.
///
/// An optional callback that configures event levels. When omitted, every lifecycle event uses
/// .
///
- /// An that logs the lifecycle of every enqueued operation.
+ /// A new immutable scheduler snapshot that logs operation lifecycles.
///
- /// Thrown when or is null.
+ /// or is null.
///
///
///
- /// The decorator emits enqueue, start, cancellation-request, success or failure, and finish events. Events
- /// contain structured operation ID, optional operation name, result type, and elapsed-duration fields where
- /// applicable. Failure events include the operation exception.
+ /// The compound middleware emits enqueue, start, cancellation-request, success or failure, and finish events.
+ /// Every event carries an increasing operation ID and the visible when
+ /// this logging registration is created. Result type and elapsed duration are included where applicable.
///
///
- /// is checked before logging each event and before optional timing
- /// work is started. Disabled events do not call .
+ /// Call before
+ /// WithLogging. Metadata is forward-scoped, so an operation name registered later does not retroactively
+ /// change this logging registration.
///
///
- /// Cancellation requests are observed synchronously from the cancellation token. This event does not mean
- /// that cancellation was accepted or that the operation ultimately completed as canceled.
- ///
- ///
- /// Place WithOperationName outside this decorator, for example
- /// scheduler.WithLogging(logger).WithOperationName("Import"), so logging can observe the annotation.
+ /// is checked before each event and before optional timing work begins.
+ /// Disabled events do not invoke .
+ /// Cancellation-request logging observes the submitting caller's token and does not imply that the operation
+ /// ultimately completes as canceled. Logging never suppresses or replaces the operation outcome.
///
+ /// The returned pipeline snapshot is non-owning and does not dispose the underlying scheduler or logger.
///
///
///
/// var scheduler = taskFlow
- /// .WithLogging(logger, options =>
+ /// .WithOperationName("imports.run")
+ /// .WithLogging(logger, options =>
/// {
+ /// options.StartedLogLevel = LogLevel.Information;
/// options.FailedLogLevel = LogLevel.Error;
/// options.FinishedLogLevel = LogLevel.Debug;
- /// })
- /// .WithOperationName("Import");
+ /// });
///
- /// await scheduler.Enqueue(() => ImportAsync());
+ /// await scheduler.Enqueue(token => ImportAsync(token));
///
///
public static ITaskScheduler WithLogging(this ITaskScheduler taskScheduler, ILogger logger, Action? configure = null)
{
Argument.NotNull(taskScheduler);
Argument.NotNull(logger);
-
var options = new TaskFlowLoggingOptions();
configure?.Invoke(options);
- return new LoggingTaskSchedulerWrapper(taskScheduler, logger, options);
+ return taskScheduler.UseMiddleware(new LoggingMiddleware(logger, options));
}
- private sealed class LoggingTaskSchedulerWrapper : ITaskScheduler
+ private sealed class LoggingMiddleware : ITaskSchedulerEnqueueMiddleware, ITaskSchedulerExecutionMiddleware
{
private readonly ILogger _logger;
private readonly TaskFlowLoggingOptions _options;
- private readonly ITaskScheduler _interceptedScheduler;
private long _lastOperationId;
- public LoggingTaskSchedulerWrapper(ITaskScheduler taskScheduler, ILogger logger, TaskFlowLoggingOptions options)
+ public LoggingMiddleware(ILogger logger, TaskFlowLoggingOptions options)
{
_logger = logger;
_options = options;
- _interceptedScheduler = taskScheduler.Intercept(new LoggingInterceptor(logger, options));
}
- public async Task Enqueue(Func