From 1d61807e979b28183779a928ae78f7db5add85e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20St=C4=99pie=C5=84?= Date: Thu, 8 Aug 2024 19:13:21 +0200 Subject: [PATCH] Add cancellation tokens --- README.md | 65 +++++++++++++--- .../AsyncResponsibilityChainTests.cs | 57 +++++++++++++- .../Pipelines/AsyncPipelineTests.cs | 37 +++++++++ src/PipelineNet/AsyncBaseMiddlewareFlow.cs | 64 ++++++++++++++++ .../AsyncResponsibilityChain.cs | 75 ++++++++++++++++--- .../IAsyncResponsibilityChain.cs | 32 +++++++- .../IResponsibilityChain.cs | 2 +- .../ResponsibilityChain.cs | 17 +++-- .../ICancellableAsyncMiddleware.WithReturn.cs | 23 ++++++ .../Middleware/ICancellableAsyncMiddleware.cs | 22 ++++++ .../MiddlewareResolver/IMiddlewareResolver.cs | 7 +- src/PipelineNet/PipelineNet.csproj | 4 +- src/PipelineNet/Pipelines/AsyncPipeline.cs | 51 ++++++++++--- src/PipelineNet/Pipelines/IAsyncPipeline.cs | 20 ++++- src/PipelineNet/Pipelines/Pipeline.cs | 15 ++-- 15 files changed, 437 insertions(+), 54 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..7c16fcc 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) @@ -174,8 +177,8 @@ As we already have an example of a chain of responsibility, here is an example u If you want to, you can use the asynchronous version, using asynchronous middleware. Changing the instantiation to: ```C# var exceptionHandlersChain = new AsyncResponsibilityChain(new ActivatorMiddlewareResolver()) - .Chain() // The order of middleware being chained matters - .Chain() + .Chain() // The order of middleware being chained matters + .Chain() .Finally((ex) => { ex.Source = ExceptionSource; @@ -194,6 +197,50 @@ result = await exceptionHandlersChain.Execute(new ArgumentException()); // Resul result = await exceptionHandlersChain.Execute(new InvalidOperationException()); // Result will be false ``` +## Cancellation tokens +If you want to, you can pass the cancellation token to your asynchronous pipeline middleware 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; + } +} +``` +To pass the cancellation token to the chain of responsibility middleware, implement the `ICancellableAsyncMiddleware` interface instead +and pass the cancellation token argument to the `IAsynchChainOfResponsibility.Execute` method. +There is the `CancellableFinally` method that accepts the cancellation token parameter: +```C# +var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) + .ChainCancellable() + .ChainCancellable() + .CancellableFinally((parameter, cancellationToken) => + { + // Do something + return true; + }); +``` + ## 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..9979f4e 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] @@ -166,8 +175,6 @@ public void Chain_AddTypeThatIsNotAMiddleware_ThrowsException() }); } - - /// /// Try to generate a deadlock in synchronous middleware. /// @@ -184,5 +191,51 @@ 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)); + } + + [Fact] + public async Task Execute_ChainOfMiddlewareWithCancellableFinally_CancellableFinallyIsExecuted() + { + var responsibilityChain = new AsyncResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain(typeof(InvalidateDataExceptionHandler)) + .Chain() + .CancellableFinally(async (ex, ct) => + { + ct.ThrowIfCancellationRequested(); + + await Task.CompletedTask; + return true; + }); + + // Creates an ArgumentNullException. The 'finally' + // function 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 'finally' function 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..ae0e516 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,10 +11,10 @@ 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; + private Func> _finallyFunc; /// /// Creates a new asynchronous chain of responsibility. @@ -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,29 +89,46 @@ 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, // the "next" function is assigned to the finally function or a // default empty function. if (index == MiddlewareTypes.Count) - func = this._finallyFunc ?? ((p) => Task.FromResult(default(TReturn))); + { + if (this._finallyFunc != null) + func = (p) => this._finallyFunc(p, cancellationToken); + else + func = (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 { @@ -120,12 +158,25 @@ 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 chain can only have one finally function. Calling this method + /// a second time will just replace the existing finally . /// /// The function that will be execute at the end of chain. /// The current instance of . public IAsyncResponsibilityChain Finally(Func> finallyFunc) + { + this._finallyFunc = (paramaeter, token) => finallyFunc(paramaeter); + return this; + } + + /// + /// 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 + /// a second time will just replace the existing finally . + /// + /// The function that will be execute at the end of chain. + /// The current instance of . + public IAsyncResponsibilityChain CancellableFinally(Func> finallyFunc) { this._finallyFunc = finallyFunc; return this; diff --git a/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs index 44ec02f..e5dbd77 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 @@ -13,13 +14,22 @@ public interface IAsyncResponsibilityChain { /// /// 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 chain can only have one finally function. Calling this method + /// a second time will just replace the existing finally . /// /// The function that will be execute at the end of chain. /// The current instance of . IAsyncResponsibilityChain Finally(Func> finallyFunc); + /// + /// 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 + /// a second time will just replace the existing finally . + /// + /// The function that will be execute at the end of chain. + /// The current instance of . + IAsyncResponsibilityChain CancellableFinally(Func> finallyFunc); + /// /// Chains a new middleware to the chain of responsibility. /// Middleware will be executed in the same order they are added. @@ -29,13 +39,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 +64,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/IResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/IResponsibilityChain.cs index 7cf5af2..a42ea71 100644 --- a/src/PipelineNet/ChainsOfResponsibility/IResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/IResponsibilityChain.cs @@ -12,7 +12,7 @@ public interface IResponsibilityChain { /// /// 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 chain can only have one finally function. Calling this method /// a second time will just replace the existing finally . /// /// The that will be execute at the end of chain. diff --git a/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs index 9ad61d4..2ce7c48 100644 --- a/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs @@ -24,7 +24,7 @@ public ResponsibilityChain(IMiddlewareResolver middlewareResolver) : base(middle /// /// 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 chain can only have one finally function. Calling this method /// a second time will just replace the existing finally . /// /// The that will be execute at the end of chain. @@ -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/MiddlewareResolver/IMiddlewareResolver.cs b/src/PipelineNet/MiddlewareResolver/IMiddlewareResolver.cs index 4abd919..321c6a9 100644 --- a/src/PipelineNet/MiddlewareResolver/IMiddlewareResolver.cs +++ b/src/PipelineNet/MiddlewareResolver/IMiddlewareResolver.cs @@ -3,16 +3,17 @@ namespace PipelineNet.MiddlewareResolver { /// - /// Used to create instances of middleware. + /// Used to resolve instances of middleware. /// You can implement this interface for your preferred dependency injection container. /// public interface IMiddlewareResolver { /// - /// Creates an instance of the give middleware type. + /// Resolves an instance of the given middleware type. /// - /// The middleware type that will be created. + /// The middleware type that will be resolved. /// An instance of the middleware. + /// If the instance of the given middleware type could not be resolved, the exception should be thrown rather than returning null or partial result. MiddlewareResolverResult Resolve(Type type); } } diff --git a/src/PipelineNet/PipelineNet.csproj b/src/PipelineNet/PipelineNet.csproj index 750417c..8bcd20e 100644 --- a/src/PipelineNet/PipelineNet.csproj +++ b/src/PipelineNet/PipelineNet.csproj @@ -9,11 +9,11 @@ PipelineNet PipelineNet $(Version) - Israel Valverde + Israel Valverde, Mariusz Stępień A micro framework that helps you implement pipeline and chain of responsibility patterns. https://github.com/ipvalverde/PipelineNet MIT - Copyright © Israel Valverde + Copyright © Israel Valverde, Mariusz Stępień Pipeline .NetCore Portable Chain Responsibility ChainOfResponsibility Core NetStandard README.md diff --git a/src/PipelineNet/Pipelines/AsyncPipeline.cs b/src/PipelineNet/Pipelines/AsyncPipeline.cs index f5dbb89..d2aa561 100644 --- a/src/PipelineNet/Pipelines/AsyncPipeline.cs +++ b/src/PipelineNet/Pipelines/AsyncPipeline.cs @@ -1,16 +1,17 @@ using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.Pipelines { /// - /// An asynchronous pipeline stores middleware that are executed when is called. + /// An asynchronous pipeline stores middleware that are executed when is called. /// 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..4506fd1 100644 --- a/src/PipelineNet/Pipelines/IAsyncPipeline.cs +++ b/src/PipelineNet/Pipelines/IAsyncPipeline.cs @@ -1,11 +1,12 @@ using PipelineNet.Middleware; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.Pipelines { /// - /// An asynchronous pipeline stores middleware that are executed when is called. + /// An asynchronous pipeline stores middleware that are executed when is called. /// The middleware are executed in the same order they are added. /// /// The type that will be the input for all the middleware. @@ -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