From 68107ef3fa7549169c6aa9409d74dade3031d8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20St=C4=99pie=C5=84?= Date: Fri, 23 Aug 2024 18:38:15 +0200 Subject: [PATCH] add IFinally --- README.md | 97 ++++++++++--- .../AsyncResponsibilityChainTests.cs | 62 +++++++- .../ResponsibilityChainTests.cs | 31 +++- .../AsyncResponsibilityChain.cs | 134 +++++++++++++++++- .../IAsyncResponsibilityChain.cs | 41 +++++- .../IResponsibilityChain.cs | 23 ++- .../ResponsibilityChain.cs | 90 +++++++++++- .../Finally/IAsyncFinally.WithReturn.cs | 20 +++ .../ICancellableAsyncFinally.WithReturn.cs | 22 +++ .../Finally/IFinally.WithReturn.cs | 17 +++ 10 files changed, 509 insertions(+), 28 deletions(-) create mode 100644 src/PipelineNet/Finally/IAsyncFinally.WithReturn.cs create mode 100644 src/PipelineNet/Finally/ICancellableAsyncFinally.WithReturn.cs create mode 100644 src/PipelineNet/Finally/IFinally.WithReturn.cs diff --git a/README.md b/README.md index d75bf69..4e10601 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) +- [Middleware resolver](#middleware-resolver) + - [ServiceProvider implementation](#serviceprovider-implementation) + - [Unity implementation](#unity-implementation) +- [Migrate from PipelineNet 0.10 to 0.20](#migrate-from-pipelinenet-010-to-020) +- [License](#license) @@ -65,7 +68,7 @@ public class ArgumentExceptionHandler : IMiddleware Now we create a chain of responsibility with the middleware: ```C# var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) - .Chain() // The order of middleware being chained matters + .Chain() .Chain(); ``` Now your instance of `ResponsibilityChain` can be executed as many times as you want: @@ -81,14 +84,19 @@ result = exceptionHandlersChain.Execute(new InvalidOperationException()); // Res ``` You can even define a fallback function that will be executed after your entire chain: ```C# -var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) - .Chain() // The order of middleware being chained matters - .Chain() - .Finally((parameter) => +public class FinallyDoSomething : IFinally +{ + public bool Finally(Exception parameter) { // Do something return true; - }); + } +} + +var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain() + .Finally(); ``` Now if the same line gets executed: ```C# @@ -96,6 +104,23 @@ var result = exceptionHandlersChain.Execute(new InvalidOperationException()); // ``` The result will be true because of the function defined in the `Finally` method. +You can also opt in to throw an exception in the `Finally` method instead of returning a value: +```C# +public class FinallyThrow : IFinally +{ + public bool Finally(Exception parameter) + { + throw new InvalidOperationException("End of the chain of responsibility reached. No middleware matches returned a value."); + } +} + +var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain() + .Finally(); +``` +Now if the end of the chain was reached and no middleware matches returned a value, the exception will be thrown. + ## Pipeline vs Chain of responsibility Here is the difference between those two in PipelineNet: - Chain of responsibility: @@ -173,14 +198,19 @@ will replace the previous function defined. As we already have an example of a chain of responsibility, here is an example using the asynchronous implementation: 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() - .Finally((ex) => +public class FinallySetExceptionSource : IAsyncFinally +{ + public Task Finally(Exception parameter) { ex.Source = ExceptionSource; return Task.FromResult(true); - }); + } +} + +var exceptionHandlersChain = new AsyncResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain() + .Finally(); ``` And here is the execution: ```C# @@ -271,5 +301,36 @@ An implementation of the [middleware resolver for Unity](https://github.com/Shan Install-Package PipelineNet.Unity ``` +## Migrate from PipelineNet 0.10 to 0.20 +In PipelineNet 0.20, `Finally` overloads that use `Func` have been made obsolete. + +To migrate replace: +```C# +var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain() + .Finally((parameter) => + { + // Do something + return true; + }); +``` +With: +```C# +public class FinallyDoSomething : IFinally +{ + public bool Finally(Exception parameter) + { + // Do something + return true; + } +} + +var exceptionHandlersChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain() + .Finally(); +``` + ## License This project is licensed under MIT. Please, feel free to contribute with code, issues or tips :) diff --git a/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs b/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs index 2dcb5b2..ba78809 100644 --- a/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs +++ b/src/PipelineNet.Tests/ChainsOfResponsibility/AsyncResponsibilityChainTests.cs @@ -1,8 +1,7 @@ using PipelineNet.ChainsOfResponsibility; +using PipelineNet.Finally; using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; -using System; -using System.Threading.Tasks; using Xunit; namespace PipelineNet.Tests.ChainsOfResponsibility @@ -88,6 +87,24 @@ public async Task Run(Exception exception, Func> exe return await executeNext(exception); } } + + public class FinallyThrow : IAsyncFinally + { + public Task Finally(Exception exception) + { + throw new InvalidOperationException( + "End of the asynchronous chain of responsibility reached. No middleware matches returned a value."); + } + } + + public class FinallyThrowIfCancellationRequested : ICancellableAsyncFinally + { + public Task Finally(Exception exception, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(default(bool)); + } + } #endregion [Fact] @@ -127,6 +144,7 @@ public async Task Execute_ChainOfMiddlewareThatDoesNotHandleTheException_ChainRe Assert.Equal(default(bool), result); } +#pragma warning disable CS0618 // Type or member is obsolete [Fact] public async Task Execute_ChainOfMiddlewareWithFinallyFunc_FinallyFuncIsExecuted() { @@ -152,6 +170,7 @@ public async Task Execute_ChainOfMiddlewareWithFinallyFunc_FinallyFuncIsExecuted Assert.Equal(ExceptionSource, exception.Source); } +#pragma warning restore CS0618 // Type or member is obsolete /// /// Tests the method. @@ -168,6 +187,7 @@ public void Chain_AddTypeThatIsNotAMiddleware_ThrowsException() +#pragma warning disable CS0618 // Type or member is obsolete /// /// Try to generate a deadlock in synchronous middleware. /// @@ -184,5 +204,43 @@ public void Execute_SynchronousChainOfResponsibility_SuccessfullyExecute() Assert.Equal("Test with spaces and new lines", result); } +#pragma warning restore CS0618 // Type or member is obsolete + + [Fact] + public async Task Execute_ChainOfMiddlewareWithFinally_FinallyIsExecuted() + { + var responsibilityChain = new AsyncResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain(typeof(InvalidateDataExceptionHandler)) + .Chain() + .Finally(); + + // Creates an ArgumentNullException. The 'MyExceptionHandler' + // middleware should be the last one to execute. + var exception = new ArgumentNullException(); + + // The 'FinallyThrow' should throw 'InvalidOperationException'. + await Assert.ThrowsAsync(() => responsibilityChain.Execute(exception)); + } + + [Fact] + public async Task Execute_ChainOfMiddlewareWithCancellableFinally_CancellableFinallyIsExecuted() + { + var responsibilityChain = new AsyncResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain(typeof(InvalidateDataExceptionHandler)) + .Chain() + .CancellableFinally(); + + // Creates an ArgumentNullException. The 'MyExceptionHandler' + // 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 'FinallyThrowIfCancellationRequested' should throw 'OperationCanceledException'. + await Assert.ThrowsAsync(() => responsibilityChain.Execute(exception, cancellationToken)); + } } } diff --git a/src/PipelineNet.Tests/ChainsOfResponsibility/ResponsibilityChainTests.cs b/src/PipelineNet.Tests/ChainsOfResponsibility/ResponsibilityChainTests.cs index 65d0767..a9dc0ba 100644 --- a/src/PipelineNet.Tests/ChainsOfResponsibility/ResponsibilityChainTests.cs +++ b/src/PipelineNet.Tests/ChainsOfResponsibility/ResponsibilityChainTests.cs @@ -1,7 +1,7 @@ using PipelineNet.ChainsOfResponsibility; +using PipelineNet.Finally; using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; -using System; using Xunit; namespace PipelineNet.Tests.ChainsOfResponsibility @@ -63,6 +63,15 @@ public bool Run(Exception exception, Func executeNext) return executeNext(exception); } } + + public class FinallyThrow : IFinally + { + bool IFinally.Finally(Exception parameter) + { + throw new InvalidOperationException( + "End of the chain of responsibility reached. No middleware matches returned a value."); + } + } #endregion [Fact] @@ -102,6 +111,7 @@ public void Execute_ChainOfMiddlewareThatDoesNotHandleTheException_ChainReturnsD Assert.Equal(default(bool), result); } +#pragma warning disable CS0618 // Type or member is obsolete [Fact] public void Execute_ChainOfMiddlewareWithFinallyFunc_FinallyFuncIsExecuted() { @@ -127,6 +137,7 @@ public void Execute_ChainOfMiddlewareWithFinallyFunc_FinallyFuncIsExecuted() Assert.Equal(ExceptionSource, exception.Source); } +#pragma warning restore CS0618 // Type or member is obsolete /// /// Tests the method. @@ -140,5 +151,23 @@ public void Chain_AddTypeThatIsNotAMiddleware_ThrowsException() responsibilityChain.Chain(typeof(ResponsibilityChainTests)); }); } + + [Fact] + public void Execute_ChainOfMiddlewareWithFinally_FinallyIsExecuted() + { + var responsibilityChain = new ResponsibilityChain(new ActivatorMiddlewareResolver()) + .Chain() + .Chain(typeof(InvalidateDataExceptionHandler)) + .Chain() + .Finally(); + + // Creates an ArgumentNullException. The 'MyExceptionHandler' + // middleware should be the last one to execute. + var exception = new ArgumentNullException(); + + // The 'FinallyThrow' should throw 'InvalidOperationException'. + Assert.Throws(() => responsibilityChain.Execute(exception)); + } + } } diff --git a/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs index 5098e92..7d59eb2 100644 --- a/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/AsyncResponsibilityChain.cs @@ -1,6 +1,9 @@ -using PipelineNet.Middleware; +using PipelineNet.Finally; +using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; using System; +using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.ChainsOfResponsibility @@ -13,6 +16,18 @@ namespace PipelineNet.ChainsOfResponsibility public class AsyncResponsibilityChain : BaseMiddlewareFlow>, IAsyncResponsibilityChain { + /// + /// Stores the of the finally type. + /// + private static readonly TypeInfo FinallyTypeInfo = typeof(IAsyncFinally).GetTypeInfo(); + + /// + /// Stores the of the cancellable finally type. + /// + private static readonly TypeInfo CancellableFinallyTypeInfo = typeof(ICancellableAsyncFinally).GetTypeInfo(); + + + private Type _finallyType; private Func> _finallyFunc; /// @@ -54,7 +69,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); @@ -64,6 +87,7 @@ public async Task Execute(TParameter parameter) func = async (param) => { MiddlewareResolverResult resolverResult = null; + MiddlewareResolverResult finallyResolverResult = null; try { var type = MiddlewareTypes[index]; @@ -75,7 +99,48 @@ public async Task Execute(TParameter parameter) // 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 (_finallyType != null) + { + finallyResolverResult = MiddlewareResolver.Resolve(_finallyType); + + if (finallyResolverResult == null || finallyResolverResult.Middleware == null) + { + throw new InvalidOperationException($"'{MiddlewareResolver.GetType()}' failed to resolve finally of type '{_finallyType}'."); + } + + if (finallyResolverResult.IsDisposable && !(finallyResolverResult.Middleware is IDisposable +#if NETSTANDARD2_1_OR_GREATER + || finallyResolverResult.Middleware is IAsyncDisposable +#endif + )) + { + throw new InvalidOperationException($"'{finallyResolverResult.Middleware.GetType()}' type does not implement IDisposable" + +#if NETSTANDARD2_1_OR_GREATER + " or IAsyncDisposable" + +#endif + "."); + } + + if (finallyResolverResult.Middleware is ICancellableAsyncFinally cancellableFinally) + { + func = async (p) => await cancellableFinally.Finally(p, cancellationToken).ConfigureAwait(false); + } + else + { + var @finally = (IAsyncFinally)finallyResolverResult.Middleware; + func = async (p) => await @finally.Finally(p).ConfigureAwait(false); + } + } + else if (_finallyFunc != null) + { + func = _finallyFunc; + } + else + { + func = (p) => Task.FromResult(default(TReturn)); + } + } if (resolverResult.IsDisposable && !(middleware is IDisposable #if NETSTANDARD2_1_OR_GREATER @@ -112,12 +177,74 @@ public async Task Execute(TParameter parameter) } } } + + if (resolverResult != null && resolverResult.IsDisposable) + { + var @finally = resolverResult.Middleware; + if (@finally != null) + { +#if NETSTANDARD2_1_OR_GREATER + if (@finally is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else +#endif + if (@finally is IDisposable disposable) + { + disposable.Dispose(); + } + } + } } }; return await func(parameter).ConfigureAwait(false); } + /// + /// Sets the finally 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 finally being set. + /// The current instance of . + public IAsyncResponsibilityChain Finally() + where TFinally : IAsyncFinally => + Finally(typeof(TFinally)); + + /// + /// Sets the cancellable finally 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 cancellable finally being set. + /// The current instance of . + public IAsyncResponsibilityChain CancellableFinally() + where TCancellableFinally : ICancellableAsyncFinally => + Finally(typeof(TCancellableFinally)); + + /// + /// Sets the finally 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 or that will be execute at the end of chain. + /// The current instance of . + public IAsyncResponsibilityChain Finally(Type finallyType) + { + if (finallyType == null) throw new ArgumentNullException("finallyType"); + + bool isAssignableFromFinally = FinallyTypeInfo.IsAssignableFrom(finallyType.GetTypeInfo()) + || CancellableFinallyTypeInfo.IsAssignableFrom(finallyType.GetTypeInfo()); + if (!isAssignableFromFinally) + throw new ArgumentException( + $"The finally type must implement \"{typeof(IAsyncFinally)}\" or \"{typeof(ICancellableAsyncFinally)}\"."); + + _finallyType = finallyType; + 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 more @@ -125,6 +252,7 @@ public async Task Execute(TParameter parameter) /// /// The function that will be execute at the end of chain. /// The current instance of . + [Obsolete("This overload is obsolete. Use IAsyncResponsibilityChain.Finally or IAsyncResponsibilityChain.CancellableFinally.")] public IAsyncResponsibilityChain Finally(Func> finallyFunc) { this._finallyFunc = finallyFunc; diff --git a/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs index 44ec02f..38c4eea 100644 --- a/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/IAsyncResponsibilityChain.cs @@ -1,5 +1,7 @@ -using PipelineNet.Middleware; +using PipelineNet.Finally; +using PipelineNet.Middleware; using System; +using System.Threading; using System.Threading.Tasks; namespace PipelineNet.ChainsOfResponsibility @@ -11,6 +13,35 @@ namespace PipelineNet.ChainsOfResponsibility /// The return type of the chain. public interface IAsyncResponsibilityChain { + /// + /// Sets the finally 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 finally being set. + /// The current instance of . + IAsyncResponsibilityChain Finally() + where TFinally : IAsyncFinally; + + /// + /// Sets the cancellable finally 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 cancellable finally being set. + /// The current instance of . + IAsyncResponsibilityChain CancellableFinally() + where TCancellableFinally : ICancellableAsyncFinally; + + /// + /// Sets the finally 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 or that will be execute at the end of chain. + /// The current instance of . + IAsyncResponsibilityChain Finally(Type finallyType); + /// /// 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 @@ -18,6 +49,7 @@ public interface IAsyncResponsibilityChain /// /// The function that will be execute at the end of chain. /// The current instance of . + [Obsolete("This overload is obsolete. Use IAsyncResponsibilityChain.Finally or IAsyncResponsibilityChain.CancellableFinally.")] IAsyncResponsibilityChain Finally(Func> finallyFunc); /// @@ -45,5 +77,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..88ae5a3 100644 --- a/src/PipelineNet/ChainsOfResponsibility/IResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/IResponsibilityChain.cs @@ -1,4 +1,5 @@ -using PipelineNet.Middleware; +using PipelineNet.Finally; +using PipelineNet.Middleware; using System; namespace PipelineNet.ChainsOfResponsibility @@ -10,6 +11,25 @@ namespace PipelineNet.ChainsOfResponsibility /// The return type of the chain. public interface IResponsibilityChain { + /// + /// Sets the finally 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 finally being set. + /// The current instance of . + IResponsibilityChain Finally() + where TFinally : IFinally; + + /// + /// Sets the finally 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 that will be execute at the end of chain. + /// The current instance of . + IResponsibilityChain Finally(Type finallyType); + /// /// 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 @@ -17,6 +37,7 @@ public interface IResponsibilityChain /// /// The that will be execute at the end of chain. /// The current instance of . + [Obsolete("This overload is obsolete. Use IResponsibilityChain.Finally.")] IResponsibilityChain Finally(Func finallyFunc); /// diff --git a/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs b/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs index 9ad61d4..c8274a4 100644 --- a/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs +++ b/src/PipelineNet/ChainsOfResponsibility/ResponsibilityChain.cs @@ -1,6 +1,8 @@ -using PipelineNet.Middleware; +using PipelineNet.Finally; +using PipelineNet.Middleware; using PipelineNet.MiddlewareResolver; using System; +using System.Reflection; namespace PipelineNet.ChainsOfResponsibility { @@ -12,6 +14,12 @@ namespace PipelineNet.ChainsOfResponsibility public class ResponsibilityChain : BaseMiddlewareFlow>, IResponsibilityChain { + /// + /// Stores the of the finally type. + /// + private static readonly TypeInfo FinallyTypeInfo = typeof(IFinally).GetTypeInfo(); + + private Type _finallyType; private Func _finallyFunc; /// @@ -22,6 +30,36 @@ public ResponsibilityChain(IMiddlewareResolver middlewareResolver) : base(middle { } + /// + /// Sets the finally 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 finally being set. + /// The current instance of . + IResponsibilityChain IResponsibilityChain.Finally() => + Finally(typeof(TFinally)); + + /// + /// Sets the finally 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 that will be execute at the end of chain. + /// The current instance of . + public IResponsibilityChain Finally(Type finallyType) + { + if (finallyType == null) throw new ArgumentNullException("finallyType"); + + bool isAssignableFromFinally = FinallyTypeInfo.IsAssignableFrom(finallyType.GetTypeInfo()); + if (!isAssignableFromFinally) + throw new ArgumentException( + $"The finally type must implement \"{typeof(IFinally)}\"."); + + _finallyType = finallyType; + 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 more @@ -29,6 +67,7 @@ public ResponsibilityChain(IMiddlewareResolver middlewareResolver) : base(middle /// /// The that will be execute at the end of chain. /// The current instance of . + [Obsolete("This overload is obsolete. Use IResponsibilityChain.Finally.")] public IResponsibilityChain Finally(Func finallyFunc) { this._finallyFunc = finallyFunc; @@ -77,6 +116,7 @@ public TReturn Execute(TParameter parameter) func = (param) => { MiddlewareResolverResult resolverResult = null; + MiddlewareResolverResult finallyResolverResult = null; try { var type = MiddlewareTypes[index]; @@ -88,7 +128,41 @@ public TReturn Execute(TParameter parameter) // the "next" function is assigned to the finally function or a // default empty function. if (index == MiddlewareTypes.Count) - func = this._finallyFunc ?? ((p) => default(TReturn)); + { + if (_finallyType != null) + { + finallyResolverResult = MiddlewareResolver.Resolve(_finallyType); + + if (finallyResolverResult == null || finallyResolverResult.Middleware == null) + { + throw new InvalidOperationException($"'{MiddlewareResolver.GetType()}' failed to resolve finally of type '{_finallyType}'."); + } + + if (finallyResolverResult.IsDisposable && !(finallyResolverResult.Middleware is IDisposable)) + { +#if NETSTANDARD2_1_OR_GREATER + if (finallyResolverResult.Middleware is IAsyncDisposable) + { + throw new InvalidOperationException($"'{finallyResolverResult.Middleware.GetType()}' type only implements IAsyncDisposable." + + " Use AsyncResponsibilityChain to execute the configured pipeline."); + } +#endif + + throw new InvalidOperationException($"'{finallyResolverResult.Middleware.GetType()}' type does not implement IDisposable."); + } + + var @finally = (IFinally)finallyResolverResult.Middleware; + func = (p) => @finally.Finally(p); + } + else if (_finallyFunc != null) + { + func = _finallyFunc; + } + else + { + func = (p) => default(TReturn); + } + } if (resolverResult.IsDisposable && !(middleware is IDisposable)) { @@ -118,6 +192,18 @@ public TReturn Execute(TParameter parameter) } } } + + if (resolverResult != null && resolverResult.IsDisposable) + { + var @finally = resolverResult.Middleware; + if (@finally != null) + { + if (@finally is IDisposable disposable) + { + disposable.Dispose(); + } + } + } } }; diff --git a/src/PipelineNet/Finally/IAsyncFinally.WithReturn.cs b/src/PipelineNet/Finally/IAsyncFinally.WithReturn.cs new file mode 100644 index 0000000..52c175a --- /dev/null +++ b/src/PipelineNet/Finally/IAsyncFinally.WithReturn.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace PipelineNet.Finally +{ + /// + /// Defines the asynchronous chain of responsibility finally. + /// Finally will be executed at the end of the chain as a fallback. + /// + /// The input type for the finally. + /// The return type of the finally. + public interface IAsyncFinally + { + /// + /// Executes the finally. + /// + /// The input parameter. + /// The return value. + Task Finally(TParameter parameter); + } +} diff --git a/src/PipelineNet/Finally/ICancellableAsyncFinally.WithReturn.cs b/src/PipelineNet/Finally/ICancellableAsyncFinally.WithReturn.cs new file mode 100644 index 0000000..63d237b --- /dev/null +++ b/src/PipelineNet/Finally/ICancellableAsyncFinally.WithReturn.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PipelineNet.Finally +{ + /// + /// Defines the asynchronous chain of responsibility finally with cancellation token. + /// Finally will be executed at the end of the chain as a fallback. + /// + /// The input type for the finally. + /// The return type of the finally. + public interface ICancellableAsyncFinally + { + /// + /// Executes the finally. + /// + /// The input parameter. + /// The cancellation token. + /// The return value. + Task Finally(TParameter parameter, CancellationToken cancellationToken); + } +} diff --git a/src/PipelineNet/Finally/IFinally.WithReturn.cs b/src/PipelineNet/Finally/IFinally.WithReturn.cs new file mode 100644 index 0000000..f1d7ee7 --- /dev/null +++ b/src/PipelineNet/Finally/IFinally.WithReturn.cs @@ -0,0 +1,17 @@ +namespace PipelineNet.Finally +{ + /// + /// Defines the chain of responsibility finally. + /// Finally will be executed at the end of the chain as a fallback. + /// + /// The input type for the finally. + /// The return type of the finally. + public interface IFinally + { + /// + /// Executes the finally. + /// + /// The input parameter. + TReturn Finally(TParameter parameter); + } +}