From aec6993c9293b4f00c63ff7babf7eaaa454121fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20St=C4=99pie=C5=84?= Date: Fri, 20 Sep 2024 20:05:38 +0200 Subject: [PATCH 1/3] Add cancellation tokens --- README.md | 48 ++++++++++++-- .../AsyncResponsibilityChainTests.cs | 29 +++++++++ .../Pipelines/AsyncPipelineTests.cs | 37 +++++++++++ src/PipelineNet/AsyncBaseMiddlewareFlow.cs | 64 +++++++++++++++++++ .../AsyncResponsibilityChain.cs | 51 ++++++++++++--- .../IAsyncResponsibilityChain.cs | 19 +++++- .../ResponsibilityChain.cs | 15 +++-- .../ICancellableAsyncMiddleware.WithReturn.cs | 23 +++++++ .../Middleware/ICancellableAsyncMiddleware.cs | 22 +++++++ src/PipelineNet/Pipelines/AsyncPipeline.cs | 49 +++++++++++--- src/PipelineNet/Pipelines/IAsyncPipeline.cs | 18 +++++- src/PipelineNet/Pipelines/Pipeline.cs | 15 +++-- 12 files changed, 354 insertions(+), 36 deletions(-) create mode 100644 src/PipelineNet/AsyncBaseMiddlewareFlow.cs create mode 100644 src/PipelineNet/Middleware/ICancellableAsyncMiddleware.WithReturn.cs create mode 100644 src/PipelineNet/Middleware/ICancellableAsyncMiddleware.cs diff --git a/README.md b/README.md index d75bf69..46f819a 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,16 @@ dotnet add package PipelineNet **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - - [Simple example](#simple-example) - - [Pipeline vs Chain of responsibility](#pipeline-vs-chain-of-responsibility) - - [Middleware](#middleware) - - [Pipelines](#pipelines) - - [Chains of responsibility](#chains-of-responsibility) - - [Middleware resolver](#middleware-resolver) - - [License](#license) +- [Simple example](#simple-example) +- [Pipeline vs Chain of responsibility](#pipeline-vs-chain-of-responsibility) +- [Middleware](#middleware) +- [Pipelines](#pipelines) +- [Chains of responsibility](#chains-of-responsibility) +- [Cancellation tokens](#cancellation-tokens) +- [Middleware resolver](#middleware-resolver) + - [ServiceProvider implementation](#serviceprovider-implementation) + - [Unity implementation](#unity-implementation) +- [License](#license) @@ -194,6 +197,37 @@ result = await exceptionHandlersChain.Execute(new ArgumentException()); // Resul result = await exceptionHandlersChain.Execute(new InvalidOperationException()); // Result will be false ``` +## Cancellation tokens +If you want to pass the cancellation token to your asynchronous pipeline middleware, you can do so by implementing the `ICancellableAsyncMiddleware` interface +and passing the cancellation token argument to the `IAsyncPipeline.Execute` method: +```C# +var pipeline = new AsyncPipeline(new ActivatorMiddlewareResolver()) + .AddCancellable() + .Add() // You can mix both kinds of asynchronous middleware + .AddCancellable(); + +Bitmap image = (Bitmap) Image.FromFile("party-photo.png"); +CancellationToken cancellationToken = CancellationToken.None; +await pipeline.Execute(image, cancellationToken); + +public class RoudCornersCancellableAsyncMiddleware : ICancellableAsyncMiddleware +{ + public async Task Run(Bitmap parameter, Func next, CancellationToken cancellationToken) + { + await RoundCournersAsync(parameter, cancellationToken); + await next(parameter); + } + + private async Task RoudCournersAsync(Bitmap bitmap, CancellationToken cancellationToken) + { + // Handle somehow + await Task.CompletedTask; + } +} +``` +And to pass the cancellation token to your asynchronous chain of responsibility middleware, you can implement the `ICancellableAsyncMiddleware` interface +and pass the cancellation token argument to the `IAsynchChainOfResponsibility.Execute` method. + ## Middleware resolver You may be wondering what is all this `ActivatorMiddlewareResolver` class being passed to every instance of pipeline and chain of responsibility. This is a default implementation of the `IMiddlewareResolver`, which is used to create instances of the middleware types. diff --git a/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs b/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs index 2dcb5b2..f24bd85 100644 --- a/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs +++ b/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs @@ -88,6 +88,15 @@ public async Task Run(Exception exception, Func> exe return await executeNext(exception); } } + + public class ThrowIfCancellationRequestedMiddleware : ICancellableAsyncMiddleware + { + public async Task Run(Exception exception, Func> executeNext, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await executeNext(exception); + } + } #endregion [Fact] @@ -184,5 +193,25 @@ public void Execute_SynchronousChainOfResponsibility_SuccessfullyExecute() Assert.Equal("Test with spaces and new lines", result); } + + [Fact] + public async Task Execute_ChainOfMiddlewareWithCancellableMiddleware_CancellableMiddlewareIsExecuted() + { + var responsibilityChain = new AsyncResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain(typeof(InvalidateDataExceptionHandler)) + .Chain() + .ChainCancellable(); + + // Creates an ArgumentNullException. The 'ThrowIfCancellationRequestedMiddleware' + // middleware should be the last one to execute. + var exception = new ArgumentNullException(); + + // Create the cancellation token in the canceled state. + var cancellationToken = new CancellationToken(canceled: true); + + // The 'ThrowIfCancellationRequestedMiddleware' should throw 'OperationCanceledException'. + await Assert.ThrowsAsync(() => responsibilityChain.Execute(exception, cancellationToken)); + } } } diff --git a/src/PipelineNet.Tests/Pipelines/AsyncPipelineTests.cs b/src/PipelineNet.Tests/Pipelines/AsyncPipelineTests.cs index 804681f..321a129 100644 --- a/src/PipelineNet.Tests/Pipelines/AsyncPipelineTests.cs +++ b/src/PipelineNet.Tests/Pipelines/AsyncPipelineTests.cs @@ -71,6 +71,15 @@ public async Task Run(PersonModel context, Func executeNext) await executeNext(context); } } + + public class ThrowIfCancellationRequestedMiddleware : ICancellableAsyncMiddleware + { + public async Task Run(PersonModel context, Func executeNext, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await executeNext(context); + } + } #endregion [Fact] @@ -162,5 +171,33 @@ public void Add_AddTypeThatIsNotAMiddleware_ThrowsException() pipeline.Add(typeof(AsyncPipelineTests)); }); } + + [Fact] + public async Task Execute_RunPipelineWithCancellableMiddleware_CancellableMiddlewareIsExecuted() + { + var pipeline = new AsyncPipeline(new ActivatorMiddlewareResolver()) + .Add() + .Add() + .Add() + .Add() + .AddCancellable(); + + // Create a new instance with a 'Gender' property. The 'ThrowIfCancellationRequestedMiddleware' + // middleware should be the last one to execute. + var personModel = new PersonModel + { + Name = "this_is_my_email@servername.js", + Gender = Gender.Other + }; + + // Create the cancellation token in the canceled state. + var cancellationToken = new CancellationToken(canceled: true); + + // Check if 'ThrowIfCancellationRequestedMiddleware' threw 'OperationCanceledException'. + await Assert.ThrowsAsync(() => pipeline.Execute(personModel, cancellationToken)); + + // Check if the level of 'personModel' is 4, which is configured by 'PersonWithGenderProperty' middleware. + Assert.Equal(4, personModel.Level); + } } } diff --git a/src/PipelineNet/AsyncBaseMiddlewareFlow.cs b/src/PipelineNet/AsyncBaseMiddlewareFlow.cs new file mode 100644 index 0000000..2c4bf6b --- /dev/null +++ b/src/PipelineNet/AsyncBaseMiddlewareFlow.cs @@ -0,0 +1,64 @@ +using PipelineNet.MiddlewareResolver; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace PipelineNet +{ + /// + /// Defines the base class for asynchronous middleware flows. + /// + /// The middleware type. + /// The cancellable middleware type. + public abstract class AsyncBaseMiddlewareFlow + { + /// + /// The list of middleware types. + /// + protected IList MiddlewareTypes { get; private set; } + + /// + /// The resolver used to create the middleware types. + /// + protected IMiddlewareResolver MiddlewareResolver { get; private set; } + + internal AsyncBaseMiddlewareFlow(IMiddlewareResolver middlewareResolver) + { + MiddlewareResolver = middlewareResolver ?? throw new ArgumentNullException("middlewareResolver", + "An instance of IMiddlewareResolver must be provided. You can use ActivatorMiddlewareResolver."); + MiddlewareTypes = new List(); + } + + /// + /// Stores the of the middleware type. + /// + private static readonly TypeInfo MiddlewareTypeInfo = typeof(TMiddleware).GetTypeInfo(); + + /// + /// Stores the of the cancellable middleware type. + /// + private static readonly TypeInfo CancellableMiddlewareTypeInfo = typeof(TCancellableMiddleware).GetTypeInfo(); + + + /// + /// Adds a new middleware type to the internal list of types. + /// Middleware will be executed in the same order they are added. + /// + /// The middleware type to be executed. + /// Thrown if the is + /// not an implementation of or . + /// Thrown if is null. + protected void AddMiddleware(Type middlewareType) + { + if (middlewareType == null) throw new ArgumentNullException("middlewareType"); + + bool isAssignableFromMiddleware = MiddlewareTypeInfo.IsAssignableFrom(middlewareType.GetTypeInfo()) + || CancellableMiddlewareTypeInfo.IsAssignableFrom(middlewareType.GetTypeInfo()); + if (!isAssignableFromMiddleware) + throw new ArgumentException( + $"The middleware type must implement \"{typeof(TMiddleware)}\" or \"{typeof(TCancellableMiddleware)}\"."); + + this.MiddlewareTypes.Add(middlewareType); + } + } +} diff --git a/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs index 5098e92..d02b3f9 100644 --- a/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs @@ -1,6 +1,7 @@ using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.ChainsOfResponsibility @@ -10,7 +11,7 @@ namespace PipelineNet.ChainsOfResponsibility /// /// The input type for the chain. /// The return type of the chain. - public class AsyncResponsibilityChain : BaseMiddlewareFlow>, + public class AsyncResponsibilityChain : AsyncBaseMiddlewareFlow, ICancellableAsyncMiddleware>, IAsyncResponsibilityChain { private Func> _finallyFunc; @@ -35,13 +36,25 @@ public IAsyncResponsibilityChain Chain() where return this; } + /// + /// Chains a new cancellable middleware to the chain of responsibility. + /// Middleware will be executed in the same order they are added. + /// + /// The new middleware being added. + /// The current instance of . + public IAsyncResponsibilityChain ChainCancellable() where TCancellableMiddleware : ICancellableAsyncMiddleware + { + MiddlewareTypes.Add(typeof(TCancellableMiddleware)); + return this; + } + /// /// Chains a new middleware type to the chain of responsibility. /// Middleware will be executed in the same order they are added. /// /// The middleware type to be executed. /// Thrown if the is - /// not an implementation of . + /// not an implementation of or . /// Thrown if is null. /// The current instance of . public IAsyncResponsibilityChain Chain(Type middlewareType) @@ -54,7 +67,15 @@ public IAsyncResponsibilityChain Chain(Type middlewareType) /// Executes the configured chain of responsibility. /// /// - public async Task Execute(TParameter parameter) + public async Task Execute(TParameter parameter) => + await Execute(parameter, default).ConfigureAwait(false); + + /// + /// Executes the configured chain of responsibility. + /// + /// + /// The cancellation token that will be passed to all middleware. + public async Task Execute(TParameter parameter, CancellationToken cancellationToken) { if (MiddlewareTypes.Count == 0) return default(TReturn); @@ -68,7 +89,6 @@ public async Task Execute(TParameter parameter) { var type = MiddlewareTypes[index]; resolverResult = MiddlewareResolver.Resolve(type); - var middleware = (IAsyncMiddleware)resolverResult.Middleware; index++; // If the current instance of middleware is the last one in the list, @@ -77,20 +97,33 @@ public async Task Execute(TParameter parameter) if (index == MiddlewareTypes.Count) func = this._finallyFunc ?? ((p) => Task.FromResult(default(TReturn))); - if (resolverResult.IsDisposable && !(middleware is IDisposable + if (resolverResult == null || resolverResult.Middleware == null) + { + throw new InvalidOperationException($"'{MiddlewareResolver.GetType()}' failed to resolve middleware of type '{type}'."); + } + + if (resolverResult.IsDisposable && !(resolverResult.Middleware is IDisposable #if NETSTANDARD2_1_OR_GREATER - || middleware is IAsyncDisposable + || resolverResult.Middleware is IAsyncDisposable #endif )) { - throw new InvalidOperationException($"'{middleware.GetType().FullName}' type does not implement IDisposable" + + throw new InvalidOperationException($"'{resolverResult.Middleware.GetType()}' type does not implement IDisposable" + #if NETSTANDARD2_1_OR_GREATER " or IAsyncDisposable" + #endif "."); } - return await middleware.Run(param, func).ConfigureAwait(false); + if (resolverResult.Middleware is ICancellableAsyncMiddleware cancellableMiddleware) + { + return await cancellableMiddleware.Run(param, func, cancellationToken).ConfigureAwait(false); + } + else + { + var middleware = (IAsyncMiddleware)resolverResult.Middleware; + return await middleware.Run(param, func).ConfigureAwait(false); + } } finally { @@ -121,7 +154,7 @@ public async Task Execute(TParameter parameter) /// /// Sets the function to be executed at the end of the chain as a fallback. /// A chain can only have one finally function. Calling this method more - /// a second time will just replace the existing finally . + /// a second time will just replace the existing finally . /// /// The function that will be execute at the end of chain. /// The current instance of . diff --git a/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs index 44ec02f..b637762 100644 --- a/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs @@ -1,5 +1,6 @@ using PipelineNet.Middleware; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.ChainsOfResponsibility @@ -29,13 +30,22 @@ public interface IAsyncResponsibilityChain IAsyncResponsibilityChain Chain() where TMiddleware : IAsyncMiddleware; + /// + /// Chains a new cancellable middleware to the chain of responsibility. + /// Middleware will be executed in the same order they are added. + /// + /// The new cancellable middleware being added. + /// The current instance of . + IAsyncResponsibilityChain ChainCancellable() + where TCancellableMiddleware : ICancellableAsyncMiddleware; + /// /// Chains a new middleware type to the chain of responsibility. /// Middleware will be executed in the same order they are added. /// /// The middleware type to be executed. /// Thrown if the is - /// not an implementation of . + /// not an implementation of or . /// Thrown if is null. /// The current instance of . IAsyncResponsibilityChain Chain(Type middlewareType); @@ -45,5 +55,12 @@ IAsyncResponsibilityChain Chain() /// /// Task Execute(TParameter parameter); + + /// + /// Executes the configured chain of responsibility. + /// + /// + /// The cancellation token that will be passed to all middleware. + Task Execute(TParameter parameter, CancellationToken cancellationToken); } } diff --git a/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs index 9ad61d4..f8d5a80 100644 --- a/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs @@ -81,7 +81,6 @@ public TReturn Execute(TParameter parameter) { var type = MiddlewareTypes[index]; resolverResult = MiddlewareResolver.Resolve(type); - var middleware = (IMiddleware)resolverResult.Middleware; index++; // If the current instance of middleware is the last one in the list, @@ -90,19 +89,25 @@ public TReturn Execute(TParameter parameter) if (index == MiddlewareTypes.Count) func = this._finallyFunc ?? ((p) => default(TReturn)); - if (resolverResult.IsDisposable && !(middleware is IDisposable)) + if (resolverResult == null || resolverResult.Middleware == null) + { + throw new InvalidOperationException($"'{MiddlewareResolver.GetType()}' failed to resolve middleware of type '{type}'."); + } + + if (resolverResult.IsDisposable && !(resolverResult.Middleware is IDisposable)) { #if NETSTANDARD2_1_OR_GREATER - if (middleware is IAsyncDisposable) + if (resolverResult.Middleware is IAsyncDisposable) { - throw new InvalidOperationException($"'{middleware.GetType().FullName}' type only implements IAsyncDisposable." + + throw new InvalidOperationException($"'{resolverResult.Middleware.GetType()}' type only implements IAsyncDisposable." + " Use AsyncResponsibilityChain to execute the configured pipeline."); } #endif - throw new InvalidOperationException($"'{middleware.GetType().FullName}' type does not implement IDisposable."); + throw new InvalidOperationException($"'{resolverResult.Middleware.GetType()}' type does not implement IDisposable."); } + var middleware = (IMiddleware)resolverResult.Middleware; return middleware.Run(param, func); } finally diff --git a/src/PipelineNet/Middleware/ICancellableAsyncMiddleware.WithReturn.cs b/src/PipelineNet/Middleware/ICancellableAsyncMiddleware.WithReturn.cs new file mode 100644 index 0000000..53024cb --- /dev/null +++ b/src/PipelineNet/Middleware/ICancellableAsyncMiddleware.WithReturn.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PipelineNet.Middleware +{ + /// + /// Defines the asynchronous chain of responsibility middleware with cancellation token. + /// + /// The input type for the middleware. + /// The return type of the middleware. + public interface ICancellableAsyncMiddleware + { + /// + /// Runs the middleware. + /// + /// The input parameter. + /// The next middleware in the flow. + /// The cancellation token. + /// The return value. + Task Run(TParameter parameter, Func> next, CancellationToken cancellationToken); + } +} diff --git a/src/PipelineNet/Middleware/ICancellableAsyncMiddleware.cs b/src/PipelineNet/Middleware/ICancellableAsyncMiddleware.cs new file mode 100644 index 0000000..3d288b7 --- /dev/null +++ b/src/PipelineNet/Middleware/ICancellableAsyncMiddleware.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PipelineNet.Middleware +{ + /// + /// Defines the asynchronous pipeline middleware with cancellation token. + /// + /// The type that will be the input for the middleware. + public interface ICancellableAsyncMiddleware + { + /// + /// Runs the middleware. + /// + /// The input parameter. + /// The next middleware in the flow. + /// The cancellation token. + /// The return value. + Task Run(TParameter parameter, Func next, CancellationToken cancellationToken); + } +} diff --git a/src/PipelineNet/Pipelines/AsyncPipeline.cs b/src/PipelineNet/Pipelines/AsyncPipeline.cs index f5dbb89..579b640 100644 --- a/src/PipelineNet/Pipelines/AsyncPipeline.cs +++ b/src/PipelineNet/Pipelines/AsyncPipeline.cs @@ -1,6 +1,7 @@ using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.Pipelines @@ -10,7 +11,7 @@ namespace PipelineNet.Pipelines /// The middleware are executed in the same order they are added. /// /// The type that will be the input for all the middleware. - public class AsyncPipeline : BaseMiddlewareFlow>, IAsyncPipeline + public class AsyncPipeline : AsyncBaseMiddlewareFlow, ICancellableAsyncMiddleware>, IAsyncPipeline { public AsyncPipeline(IMiddlewareResolver middlewareResolver) : base(middlewareResolver) { } @@ -27,13 +28,25 @@ public IAsyncPipeline Add() return this; } + /// + /// Adds a cancellable middleware type to be executed. + /// + /// + /// + public IAsyncPipeline AddCancellable() + where TCancellableMiddleware : ICancellableAsyncMiddleware + { + MiddlewareTypes.Add(typeof(TCancellableMiddleware)); + return this; + } + /// /// Adds a middleware type to be executed. /// /// The middleware type to be executed. /// /// Thrown if the is - /// not an implementation of . + /// not an implementation of or . /// Thrown if is null. public IAsyncPipeline Add(Type middlewareType) { @@ -45,7 +58,15 @@ public IAsyncPipeline Add(Type middlewareType) /// Execute the configured pipeline. /// /// - public async Task Execute(TParameter parameter) + public async Task Execute(TParameter parameter) => + await Execute(parameter, default).ConfigureAwait(false); + + /// + /// Execute the configured pipeline. + /// + /// + /// The cancellation token that will be passed to all middleware. + public async Task Execute(TParameter parameter, CancellationToken cancellationToken) { if (MiddlewareTypes.Count == 0) return; @@ -59,26 +80,38 @@ public async Task Execute(TParameter parameter) { var type = MiddlewareTypes[index]; resolverResult = MiddlewareResolver.Resolve(type); - var middleware = (IAsyncMiddleware)resolverResult.Middleware; index++; if (index == MiddlewareTypes.Count) action = (p) => Task.FromResult(0); - if (resolverResult.IsDisposable && !(middleware is IDisposable + if (resolverResult == null || resolverResult.Middleware == null) + { + throw new InvalidOperationException($"'{MiddlewareResolver.GetType()}' failed to resolve middleware of type '{type}'."); + } + + if (resolverResult.IsDisposable && !(resolverResult.Middleware is IDisposable #if NETSTANDARD2_1_OR_GREATER - || middleware is IAsyncDisposable + || resolverResult.Middleware is IAsyncDisposable #endif )) { - throw new InvalidOperationException($"'{middleware.GetType().FullName}' type does not implement IDisposable" + + throw new InvalidOperationException($"'{resolverResult.Middleware.GetType()}' type does not implement IDisposable" + #if NETSTANDARD2_1_OR_GREATER " or IAsyncDisposable" + #endif "."); } - await middleware.Run(param, action).ConfigureAwait(false); + if (resolverResult.Middleware is ICancellableAsyncMiddleware cancellableMiddleware) + { + await cancellableMiddleware.Run(param, action, cancellationToken).ConfigureAwait(false); + } + else + { + var middleware = (IAsyncMiddleware)resolverResult.Middleware; + await middleware.Run(param, action).ConfigureAwait(false); + } } finally { diff --git a/src/PipelineNet/Pipelines/IAsyncPipeline.cs b/src/PipelineNet/Pipelines/IAsyncPipeline.cs index a24685f..739ad5c 100644 --- a/src/PipelineNet/Pipelines/IAsyncPipeline.cs +++ b/src/PipelineNet/Pipelines/IAsyncPipeline.cs @@ -1,5 +1,6 @@ using PipelineNet.Middleware; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.Pipelines @@ -19,19 +20,34 @@ public interface IAsyncPipeline IAsyncPipeline Add() where TMiddleware : IAsyncMiddleware; + /// + /// Adds a cancellable middleware type to be executed. + /// + /// + /// + IAsyncPipeline AddCancellable() + where TCancellableMiddleware : ICancellableAsyncMiddleware; + /// /// Execute the configured pipeline. /// /// Task Execute(TParameter parameter); + /// + /// Execute the configured pipeline. + /// + /// + /// The cancellation token that will be passed to all middleware. + Task Execute(TParameter parameter, CancellationToken cancellationToken); + /// /// Adds a middleware type to be executed. /// /// The middleware type to be executed. /// /// Thrown if the is - /// not an implementation of . + /// not an implementation of or . /// Thrown if is null. IAsyncPipeline Add(Type middlewareType); } diff --git a/src/PipelineNet/Pipelines/Pipeline.cs b/src/PipelineNet/Pipelines/Pipeline.cs index f10275e..30100ec 100644 --- a/src/PipelineNet/Pipelines/Pipeline.cs +++ b/src/PipelineNet/Pipelines/Pipeline.cs @@ -62,25 +62,30 @@ public void Execute(TParameter parameter) { var type = MiddlewareTypes[index]; resolverResult = MiddlewareResolver.Resolve(type); - var middleware = (IMiddleware)resolverResult.Middleware; index++; if (index == MiddlewareTypes.Count) action = (p) => { }; - if (resolverResult.IsDisposable && !(middleware is IDisposable)) + if (resolverResult == null || resolverResult.Middleware == null) + { + throw new InvalidOperationException($"'{MiddlewareResolver.GetType()}' failed to resolve middleware of type '{type}'."); + } + + if (resolverResult.IsDisposable && !(resolverResult.Middleware is IDisposable)) { #if NETSTANDARD2_1_OR_GREATER - if (middleware is IAsyncDisposable) + if (resolverResult.Middleware is IAsyncDisposable) { - throw new InvalidOperationException($"'{middleware.GetType().FullName}' type only implements IAsyncDisposable." + + throw new InvalidOperationException($"'{resolverResult.Middleware.GetType()}' type only implements IAsyncDisposable." + " Use AsyncPipeline to execute the configured pipeline."); } #endif - throw new InvalidOperationException($"'{middleware.GetType().FullName}' type does not implement IDisposable."); + throw new InvalidOperationException($"'{resolverResult.Middleware.GetType()}' type does not implement IDisposable."); } + var middleware = (IMiddleware)resolverResult.Middleware; middleware.Run(param, action); } finally From 3b05f305e5d9f0e8d0929090bce03495b4c7ecd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20St=C4=99pie=C5=84?= Date: Mon, 14 Oct 2024 19:00:21 +0200 Subject: [PATCH 2/3] register missing finally interface --- src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs b/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs index 9946ec4..32b81a6 100644 --- a/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs +++ b/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs @@ -35,6 +35,7 @@ public static IServiceCollection AddMiddlewareFromAssembly( typeof(IAsyncMiddleware<>), typeof(IMiddleware<,>), typeof(IAsyncMiddleware<,>), + typeof(IFinally<,>), typeof(ICancellableAsyncFinally<,>), typeof(IAsyncFinally<,>) }; From e2eaddbd8a5c72948049a7647faa4087efc6d435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20St=C4=99pie=C5=84?= Date: Mon, 14 Oct 2024 19:02:22 +0200 Subject: [PATCH 3/3] register new cancellable interfaces, reorder registrations --- .../ServiceCollectionExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs b/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs index 32b81a6..8852b60 100644 --- a/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs +++ b/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs @@ -33,11 +33,13 @@ public static IServiceCollection AddMiddlewareFromAssembly( { typeof(IMiddleware<>), typeof(IAsyncMiddleware<>), + typeof(ICancellableAsyncMiddleware<>), typeof(IMiddleware<,>), typeof(IAsyncMiddleware<,>), + typeof(ICancellableAsyncMiddleware<,>), typeof(IFinally<,>), - typeof(ICancellableAsyncFinally<,>), - typeof(IAsyncFinally<,>) + typeof(IAsyncFinally<,>), + typeof(ICancellableAsyncFinally<,>) }; var types = assembly.GetTypes();