diff --git a/src/CatchBlockHandlers/CatchBlockFilter.cs b/src/CatchBlockHandlers/CatchBlockFilter.cs index aee3b2dd..4fd691ca 100644 --- a/src/CatchBlockHandlers/CatchBlockFilter.cs +++ b/src/CatchBlockHandlers/CatchBlockFilter.cs @@ -10,7 +10,7 @@ public class CatchBlockFilter { public static CatchBlockFilter Empty() => new CatchBlockFilter(); - internal PolicyProcessor.ExceptionFilter ErrorFilter { get; } = new PolicyProcessor.ExceptionFilter(); + internal PolicyProcessor.ExceptionFilter ErrorFilter { get; set; } = new PolicyProcessor.ExceptionFilter(); public CatchBlockFilter ExcludeError(ErrorType errorType = ErrorType.Error) where TException : Exception { diff --git a/src/CatchBlockHandlers/ErrorContext.cs b/src/CatchBlockHandlers/ErrorContext.cs index c70b5650..f6557e62 100644 --- a/src/CatchBlockHandlers/ErrorContext.cs +++ b/src/CatchBlockHandlers/ErrorContext.cs @@ -1,6 +1,6 @@ namespace PoliNorError { - internal abstract class ErrorContext + public abstract class ErrorContext { protected ErrorContext(T t) { diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs index 3506dab1..e3b2c384 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs @@ -16,13 +16,14 @@ public PolicyProcessorCatchBlockAsyncHandler(PolicyResult policyResult, IBulkErr public async Task HandleAsync(Exception ex, ErrorContext errorContext = null) { - var (Result, CanProcess) = PreHandle(ex, errorContext); - if (!CanProcess) - return Result; + var shouldHandleResult = ShouldHandleException(ex, errorContext); + if(shouldHandleResult != HandleCatchBlockResult.Success) + return shouldHandleResult; var bulkProcessResult = await _bulkErrorProcessor.ProcessAsync(ex, errorContext.ToProcessingErrorContext(), _configAwait, _cancellationToken).ConfigureAwait(_configAwait); - return PostHandle(bulkProcessResult, Result); + _policyResult.AddBulkProcessorErrors(bulkProcessResult); + return bulkProcessResult.IsCanceled ? HandleCatchBlockResult.Canceled : shouldHandleResult; } } } diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs index f6878cdd..f116ec89 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs @@ -22,6 +22,18 @@ protected PolicyProcessorCatchBlockHandlerBase(PolicyResult policyResult, IBulkE _policyRuleFunc = policyRuleFunc ?? ((_) => true); } + protected HandleCatchBlockResult ShouldHandleException(Exception ex, ErrorContext errorContext) + { + if (_cancellationToken.IsCancellationRequested) + { + return HandleCatchBlockResult.Canceled; + } + return CanHandle(ex, errorContext); + } + +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("This method is obsolete")] +#pragma warning restore S1133 // Deprecated code should be removed protected (HandleCatchBlockResult Result, bool CanProcess) PreHandle(Exception ex, ErrorContext errorContext) { if (_cancellationToken.IsCancellationRequested) @@ -45,6 +57,9 @@ private HandleCatchBlockResult CanHandle(Exception ex, ErrorContext errorCont return HandleCatchBlockResult.Success; } +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("This method is obsolete")] +#pragma warning restore S1133 // Deprecated code should be removed protected HandleCatchBlockResult PostHandle(BulkProcessResult bulkProcessResult, HandleCatchBlockResult resultIfNotCanceled) { _policyResult.AddBulkProcessorErrors(bulkProcessResult); diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs index 02cc201d..f9fefc7f 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs @@ -12,13 +12,14 @@ public PolicyProcessorCatchBlockSyncHandler(PolicyResult policyResult, IBulkErro public HandleCatchBlockResult Handle(Exception ex, ErrorContext errorContext = null) { - var (Result, CanProcess) = PreHandle(ex, errorContext); - if (!CanProcess) - return Result; + var shouldHandleResult = ShouldHandleException(ex, errorContext); + if (shouldHandleResult != HandleCatchBlockResult.Success) + return shouldHandleResult; var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), _cancellationToken); - return PostHandle(bulkProcessResult, Result); + _policyResult.AddBulkProcessorErrors(bulkProcessResult); + return bulkProcessResult.IsCanceled ? HandleCatchBlockResult.Canceled : shouldHandleResult; } } } diff --git a/src/ConvertExceptionDelegates.cs b/src/ConvertExceptionDelegates.cs index 767ea80c..07da4ef7 100644 --- a/src/ConvertExceptionDelegates.cs +++ b/src/ConvertExceptionDelegates.cs @@ -17,5 +17,17 @@ public static bool ToInnerException(Exception exception, out TExcept return false; } } + + public static bool ToSubException(Exception exception, out TException typedException) where TException : Exception + { + if (exception is TException found) + { + typedException = found; + return true; + } + + typedException = null; + return false; + } } } diff --git a/src/ErrorProcessorRegistration.ForErrorContext.cs b/src/ErrorProcessorRegistration.ForErrorContext.cs index b2ca6501..72b9c799 100644 --- a/src/ErrorProcessorRegistration.ForErrorContext.cs +++ b/src/ErrorProcessorRegistration.ForErrorContext.cs @@ -4,41 +4,27 @@ namespace PoliNorError { - public static partial class ErrorProcessorRegistration - { - internal static T WithErrorContextProcessorOf(this T policyProcessor, Action> actionProcessor) where T : IPolicyProcessor - { - return policyProcessor.WithErrorContextProcessorOf(actionProcessor, _addErrorProcessorAction); - } - - internal static T WithErrorContextProcessorOf(this T policyProcessor, Action> actionProcessor, CancellationType cancellationType) where T : IPolicyProcessor - { - return policyProcessor.WithErrorContextProcessorOf(actionProcessor, cancellationType, _addErrorProcessorAction); - } - - internal static T WithErrorContextProcessorOf(this T policyProcessor, Action, CancellationToken> actionProcessor) where T : IPolicyProcessor - { - return policyProcessor.WithErrorContextProcessorOf(actionProcessor, _addErrorProcessorAction); - } - - internal static T WithErrorContextProcessorOf(this T policyProcessor, Func, Task> funcProcessor) where T : IPolicyProcessor - { - return policyProcessor.WithErrorContextProcessorOf(funcProcessor, _addErrorProcessorAction); - } - - internal static T WithErrorContextProcessorOf(this T policyProcessor, Func, Task> funcProcessor, CancellationType cancellationType) where T : IPolicyProcessor - { - return policyProcessor.WithErrorContextProcessorOf(funcProcessor, cancellationType, _addErrorProcessorAction); - } - - internal static T WithErrorContextProcessorOf(this T policyProcessor, Func, CancellationToken, Task> funcProcessor) where T : IPolicyProcessor - { - return policyProcessor.WithErrorContextProcessorOf(funcProcessor, _addErrorProcessorAction); - } - - internal static T WithErrorContextProcessor(this T policyProcessor, DefaultErrorProcessor errorProcessor) where T : IPolicyProcessor - { - return policyProcessor.WithErrorProcessor(errorProcessor, _addErrorProcessorAction); - } - } -} + public static partial class ErrorProcessorRegistration + { + internal static T WithErrorContextProcessorOf(this T policyProcessor, Action> actionProcessor) where T : IPolicyProcessor + => policyProcessor.WithErrorContextProcessorOf(actionProcessor, _addErrorProcessorAction); + + internal static T WithErrorContextProcessorOf(this T policyProcessor, Action> actionProcessor, CancellationType cancellationType) where T : IPolicyProcessor + => policyProcessor.WithErrorContextProcessorOf(actionProcessor, cancellationType, _addErrorProcessorAction); + + internal static T WithErrorContextProcessorOf(this T policyProcessor, Action, CancellationToken> actionProcessor) where T : IPolicyProcessor + => policyProcessor.WithErrorContextProcessorOf(actionProcessor, _addErrorProcessorAction); + + internal static T WithErrorContextProcessorOf(this T policyProcessor, Func, Task> funcProcessor) where T : IPolicyProcessor + => policyProcessor.WithErrorContextProcessorOf(funcProcessor, _addErrorProcessorAction); + + internal static T WithErrorContextProcessorOf(this T policyProcessor, Func, Task> funcProcessor, CancellationType cancellationType) where T : IPolicyProcessor + => policyProcessor.WithErrorContextProcessorOf(funcProcessor, cancellationType, _addErrorProcessorAction); + + internal static T WithErrorContextProcessorOf(this T policyProcessor, Func, CancellationToken, Task> funcProcessor) where T : IPolicyProcessor + => policyProcessor.WithErrorContextProcessorOf(funcProcessor, _addErrorProcessorAction); + + internal static T WithErrorContextProcessor(this T policyProcessor, DefaultErrorProcessor errorProcessor) where T : IPolicyProcessor + => policyProcessor.WithErrorProcessor(errorProcessor, _addErrorProcessorAction); + } +} diff --git a/src/ErrorProcessorRegistration.ForInnerError.cs b/src/ErrorProcessorRegistration.ForInnerError.cs index 07322e64..7456bb71 100644 --- a/src/ErrorProcessorRegistration.ForInnerError.cs +++ b/src/ErrorProcessorRegistration.ForInnerError.cs @@ -7,63 +7,39 @@ namespace PoliNorError public static partial class ErrorProcessorRegistration { internal static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, CancellationType cancellationType) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(actionProcessor, cancellationType, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(actionProcessor, cancellationType, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, CancellationType cancellationType) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(funcProcessor, cancellationType, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(funcProcessor, cancellationType, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(actionProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, CancellationType cancellationType) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(actionProcessor, cancellationType, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(actionProcessor, cancellationType, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, CancellationType cancellationType) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(funcProcessor, cancellationType, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(funcProcessor, cancellationType, _addErrorProcessorAction); internal static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor) where T : IPolicyProcessor where TException : Exception - { - return policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); - } + => policyProcessor.WithInnerErrorProcessorOf(funcProcessor, _addErrorProcessorAction); } } diff --git a/src/ErrorProcessors/BulkErrorProcessor.cs b/src/ErrorProcessors/BulkErrorProcessor.cs index c9ede63a..a49ddb00 100644 --- a/src/ErrorProcessors/BulkErrorProcessor.cs +++ b/src/ErrorProcessors/BulkErrorProcessor.cs @@ -233,6 +233,8 @@ public class BulkProcessResult { private readonly bool _isCanceledBetweenProcessOne; + private readonly IReadOnlyList _processErrors; + /// /// Initializes a new instance of the class. /// @@ -242,7 +244,7 @@ public class BulkProcessResult public BulkProcessResult(Exception handlingError, IEnumerable processErrors, bool isCanceledBetweenProcessOne = false) { HandlingError = handlingError; - ProcessErrors = processErrors; + _processErrors = (processErrors ?? Array.Empty()).ToList(); _isCanceledBetweenProcessOne = isCanceledBetweenProcessOne; } @@ -254,12 +256,17 @@ public BulkProcessResult(Exception handlingError, IEnumerable /// Gets a collection of exceptions that occurred within the error processors. /// - public IEnumerable ProcessErrors { get; } + public IEnumerable ProcessErrors => _processErrors; + + /// + /// Gets a value indicating whether any processing errors occurred. + /// + public bool HasProcessErrors => _processErrors.Count > 0; /// /// Gets a value indicating whether the bulk processing operation was canceled. /// - public bool IsCanceled => ProcessErrors.Any(e => e.ErrorStatus == ProcessStatus.Canceled) || _isCanceledBetweenProcessOne; + public bool IsCanceled => _processErrors.Any(e => e.ErrorStatus == ProcessStatus.Canceled) || _isCanceledBetweenProcessOne; /// /// Converts the processing errors into a collection of . @@ -267,9 +274,13 @@ public BulkProcessResult(Exception handlingError, IEnumerableAn enumerable of . public IEnumerable ToCatchBlockExceptions() { - return ProcessErrors?.Any() != true - ? Array.Empty() - : ProcessErrors.Select(pe => new CatchBlockException(pe, HandlingError, CatchBlockExceptionSource.ErrorProcessor)); + foreach (var pe in _processErrors) + { + yield return new CatchBlockException( + pe, + HandlingError, + CatchBlockExceptionSource.ErrorProcessor); + } } } } diff --git a/src/ErrorProcessors/DefaultErrorProcessor.T.cs b/src/ErrorProcessors/DefaultErrorProcessor.T.cs index 4c5c6d91..9e04a374 100644 --- a/src/ErrorProcessors/DefaultErrorProcessor.T.cs +++ b/src/ErrorProcessors/DefaultErrorProcessor.T.cs @@ -62,7 +62,7 @@ public static DefaultErrorProcessorT Create(Action gpi) + if (TryGetProcessingErrorInfo(pi, out var gpi)) actionProcessor(ex, gpi, token); } var res = new DefaultErrorProcessorT(); @@ -90,7 +90,7 @@ public static DefaultErrorProcessorT Create(Func gpi) + if (TryGetProcessingErrorInfo(pi, out var gpi)) return funcProcessor(ex, gpi, token); else return Task.CompletedTask; @@ -112,7 +112,7 @@ private static Action ConvertToNonGenericAction< { return (Exception ex, ProcessingErrorInfo pi) => { - if (pi is ProcessingErrorInfo gpi) + if (TryGetProcessingErrorInfo(pi, out var gpi)) actionProcessor(ex, gpi); }; } @@ -121,13 +121,26 @@ private static Func ConvertToNonGenericFun { return (Exception ex, ProcessingErrorInfo pi) => { - if (pi is ProcessingErrorInfo gpi) + if (TryGetProcessingErrorInfo(pi, out var gpi)) return funcProcessor(ex, gpi); else return Task.CompletedTask; }; } + public static bool TryGetProcessingErrorInfo( + ProcessingErrorInfo pi, + out ProcessingErrorInfo gpi) + { + if (pi is ProcessingErrorInfo result) + { + gpi = result; + return true; + } + gpi = null; + return false; + } + protected override Func ParameterConverter => (_) => _; } } diff --git a/src/ErrorProcessors/DefaultTypedErrorProcessor.cs b/src/ErrorProcessors/DefaultTypedErrorProcessor.cs new file mode 100644 index 00000000..34fe445c --- /dev/null +++ b/src/ErrorProcessors/DefaultTypedErrorProcessor.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoliNorError +{ + public class DefaultTypedErrorProcessor : IErrorProcessor where TException : Exception + { + private readonly DefaultTypedErrorProcessorT _errorProcessor; + + public DefaultTypedErrorProcessor(Action actionProcessor) + { + _errorProcessor = DefaultTypedErrorProcessorT.Create(actionProcessor); + } + + public Exception Process(Exception error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, CancellationToken cancellationToken = default) + { + return _errorProcessor.Process(error, catchBlockProcessErrorInfo, cancellationToken); + } + + public async Task ProcessAsync(Exception error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, bool configAwait = false, CancellationToken cancellationToken = default) + { + return await _errorProcessor.ProcessAsync(error, catchBlockProcessErrorInfo, configAwait, cancellationToken).ConfigureAwait(configAwait); + } + } + + internal class DefaultTypedErrorProcessorT : ErrorProcessorBase where TException : Exception + { + public static DefaultTypedErrorProcessorT Create(Action actionProcessor) + { + var action = ErrorProcessorFuncConverter.Convert(actionProcessor, ConvertExceptionDelegates.ToSubException); + + var res = new DefaultTypedErrorProcessorT(); + res.SetSyncRunner(action); + return res; + } + + protected override Func ParameterConverter => (_) => _; + } +} diff --git a/src/ErrorProcessors/ICanAddErrorProcessor.cs b/src/ErrorProcessors/ICanAddErrorProcessor.cs index 7324b61e..d3d91302 100644 --- a/src/ErrorProcessors/ICanAddErrorProcessor.cs +++ b/src/ErrorProcessors/ICanAddErrorProcessor.cs @@ -69,99 +69,61 @@ public static T WithErrorProcessorOf(this T policyProcessor, Func WithErrorProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor, actionProcessor, cancellationType), action); public static T WithErrorContextProcessorOf(this T policyProcessor, Action> actionProcessor, Action action) where T : ICanAddErrorProcessor - { - return WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(actionProcessor), action); - } + => WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(actionProcessor), action); public static T WithErrorContextProcessorOf(this T policyProcessor, Action> actionProcessor, CancellationType cancellationType, Action action) where T : ICanAddErrorProcessor - { - return WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(actionProcessor, cancellationType), action); - } + => WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(actionProcessor, cancellationType), action); public static T WithErrorContextProcessorOf(this T policyProcessor, Action, CancellationToken> actionProcessor, Action action) where T : ICanAddErrorProcessor - { - return WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(actionProcessor), action); - } + => WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(actionProcessor), action); public static T WithErrorContextProcessorOf(this T policyProcessor, Func, Task> funcProcessor, Action action) where T : ICanAddErrorProcessor - { - return WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor), action); - } + => WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor), action); public static T WithErrorContextProcessorOf(this T policyProcessor, Func, Task> funcProcessor, CancellationType cancellationType, Action action) where T : ICanAddErrorProcessor - { - return WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor, cancellationType), action); - } + => WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor, cancellationType), action); public static T WithErrorContextProcessorOf(this T policyProcessor, Func, CancellationToken, Task> funcProcessor, Action action) where T : ICanAddErrorProcessor - { - return WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor), action); - } + => WithErrorContextProcessor(policyProcessor, new DefaultErrorProcessor(funcProcessor), action); public static T WithErrorContextProcessor(this T policyProcessor, DefaultErrorProcessor errorProcessor, Action action) where T : ICanAddErrorProcessor - { - return WithErrorProcessor(policyProcessor, errorProcessor, action); - } + => WithErrorProcessor(policyProcessor, errorProcessor, action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, CancellationType cancellationType, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), cancellationType, action); - } + => policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), cancellationType, action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, CancellationType cancellationType, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), cancellationType, action); - } + => policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), cancellationType, action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Action actionProcessor, CancellationType cancellationType, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), cancellationType, action); - } + => policyProcessor.WithErrorProcessorOf(actionProcessor.ToActionForInnerException(), cancellationType, action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, CancellationType cancellationType, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), cancellationType, action); - } + => policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), cancellationType, action); public static T WithInnerErrorProcessorOf(this T policyProcessor, Func funcProcessor, Action action) where T : ICanAddErrorProcessor where TException : Exception - { - return policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); - } + => policyProcessor.WithErrorProcessorOf(funcProcessor.ToFuncForInnerException(), action); internal static T WithErrorProcessor(this T policyProcessor, IErrorProcessor errorProcessor, Action action) where T : ICanAddErrorProcessor { diff --git a/src/ErrorProcessors/TypedErrorProcessor.cs b/src/ErrorProcessors/TypedErrorProcessor.cs new file mode 100644 index 00000000..f5871ef4 --- /dev/null +++ b/src/ErrorProcessors/TypedErrorProcessor.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoliNorError +{ + /// + /// Provides an abstract base class for implementing custom error processing logic that operates on exceptions + /// of a specific type . + /// + /// + /// The specific type of that this processor is designed to handle. + /// + /// + /// + /// This class implements the interface and provides a common structure for both + /// synchronous and asynchronous error processing. It delegates the core processing logic to the abstract + /// method, which derived classes must implement for the specific exception type. + /// + /// + public abstract class TypedErrorProcessor : IErrorProcessor where TException : Exception + { + private readonly DefaultTypedErrorProcessor _errorProcessor; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The constructor wires the method into the internal + /// pipeline. + /// + protected TypedErrorProcessor() + { + _errorProcessor = new DefaultTypedErrorProcessor(Execute); + } + + /// + /// Processes the given exception synchronously by invoking the internal typed processor. + /// + /// The exception to be processed. + /// Optional information about the context in which the exception was caught. + /// A cancellation token. + /// The original exception after processing. + /// + /// This method serves as a synchronous wrapper that triggers the internal processing pipeline, + /// eventually calling the overridden method if the exception type matches. + /// + public Exception Process(Exception error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, CancellationToken cancellationToken = default) + { + _errorProcessor.Process(error, catchBlockProcessErrorInfo, cancellationToken); + return error; + } + + /// + /// Processes the given exception synchronously by invoking the method and returning the result via Task.FromResult(error). + /// + /// The exception to be processed. + /// Optional information about the context in which the exception was caught. + /// A flag to configure the awaiter (not used in this implementation but available for compatibility). + /// A cancellation token. + /// A task that represents the asynchronous operation, containing the original exception after processing. + /// + /// This method provides an asynchronous signature for the error processing logic, though the base implementation + /// performs the work synchronously. + /// + public Task ProcessAsync(Exception error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, bool configAwait = false, CancellationToken cancellationToken = default) + { + Process(error, catchBlockProcessErrorInfo, cancellationToken); + return Task.FromResult(error); + } + + /// + /// The core processing logic for the specific exception type. This method must be implemented by inheriting classes. + /// + /// The typed exception to be processed. + /// Optional information about the context in which the exception was caught. + /// A cancellation token. + /// + /// Inheritors of this class MUST implement this method to define the specific logic + /// for handling exceptions of type , such as specialized logging or alerting. + /// + public abstract void Execute(TException error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, CancellationToken token = default); + } +} diff --git a/src/ErrorSet.cs b/src/ErrorSet.cs index 8ae3a1a7..1140a20f 100644 --- a/src/ErrorSet.cs +++ b/src/ErrorSet.cs @@ -30,6 +30,34 @@ public static ErrorSet FromError() where TException : Exception return new ErrorSet().WithError(); } + /// + /// Creates the set and adds items with the and types and kind to the new set. + /// + /// The first type of exception. + /// The second type of exception. + /// A new containing the specified error types. + public static ErrorSet FromErrors() + where TException1 : Exception + where TException2 : Exception + { + return new ErrorSet().WithError().WithError(); + } + + /// + /// Creates the set and adds items with the , , and types and kind to the new set. + /// + /// The first type of exception. + /// The second type of exception. + /// The third type of exception. + /// A new containing the specified error types. + public static ErrorSet FromErrors() + where TException1 : Exception + where TException2 : Exception + where TException3 : Exception + { + return new ErrorSet().WithError().WithError().WithError(); + } + /// /// Creates the set and adds the item with the type and kind to the new set. /// @@ -70,6 +98,42 @@ private ErrorSet() /// public IEnumerable Items => _set; + /// + /// Checks whether the set contains the specified exception type for an exception itself. + /// + /// The exception type to check. + /// true if the exception type exists in the set; otherwise, false. + public bool HasError() where TException : Exception + { + return HasError(typeof(TException), ErrorSetItem.ItemType.Error); + } + + /// + /// Checks whether the set contains the specified exception type for an inner exception. + /// + /// The exception type to check. + /// true if the exception type exists in the set; otherwise, false. + public bool HasInnerError() where TInnerException : Exception + { + return HasError(typeof(TInnerException), ErrorSetItem.ItemType.InnerError); + } + + /// + /// Checks whether the set contains the specified exception type with the given kind. + /// + /// The exception type to check. + /// The kind of exception to check for. + /// true if the exception type exists in the set; otherwise, false. + private bool HasError(Type exceptionType, ErrorSetItem.ItemType errorType) + { + if (exceptionType == null) + { + return false; + } + + return _set.Contains(new ErrorSetItem(exceptionType, errorType)); + } + /// /// Represents the type and kind of an exception. /// diff --git a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs index d3c16a1e..23d96c5e 100644 --- a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs +++ b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using static PoliNorError.CatchBlockFilter; namespace PoliNorError { @@ -12,6 +13,70 @@ public class ExceptionFilter public IEnumerable>> ExcludedErrorFilters => FilterSet.ExcludedErrorFilters; + public ExceptionFilter IncludeErrorSet(IErrorSet errorSet) + { + this.AddIncludedErrorSet(errorSet); + return this; + } + + public ExceptionFilter ExcludeErrorSet(IErrorSet errorSet) + { + this.AddExcludedErrorSet(errorSet); + return this; + } + + public ExceptionFilter IncludeError(ErrorType errorType = ErrorType.Error) where TException : Exception + { + return IncludeError(null, errorType); + } + + public ExceptionFilter IncludeError(Func func, ErrorType errorType = ErrorType.Error) where TException : Exception + { + switch (errorType) + { + case ErrorType.Error: + AddIncludedErrorFilter(func); + return this; + case ErrorType.InnerError: + AddIncludedInnerErrorFilter(func); + return this; + default: + throw new NotImplementedException(); + } + } + + public ExceptionFilter IncludeError(Expression> expression) + { + AddIncludedErrorFilter(expression); + return this; + } + + public ExceptionFilter ExcludeError(ErrorType errorType = ErrorType.Error) where TException : Exception + { + return ExcludeError(null, errorType); + } + + public ExceptionFilter ExcludeError(Func func, ErrorType errorType = ErrorType.Error) where TException : Exception + { + switch (errorType) + { + case ErrorType.Error: + AddExcludedErrorFilter(func); + return this; + case ErrorType.InnerError: + AddExcludedInnerErrorFilter(func); + return this; + default: + throw new NotImplementedException(); + } + } + + public ExceptionFilter ExcludeError(Expression> expression) + { + AddExcludedErrorFilter(expression); + return this; + } + internal ExceptionFilterSet FilterSet { get; } = new ExceptionFilterSet(); internal void AddIncludedErrorFilter(Expression> handledErrorFilter) diff --git a/src/Exceptions/ServiceOperationCanceledException.cs b/src/Exceptions/ServiceOperationCanceledException.cs new file mode 100644 index 00000000..e7904e34 --- /dev/null +++ b/src/Exceptions/ServiceOperationCanceledException.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; + +namespace PoliNorError +{ + /// + /// Exception used by the library as a marker for logical cancellation. + /// Created by the library (not by a ) in scenarios such as + /// when a synchronous policy processor handles a delegate that waits on one or multiple tasks + /// (for example, via Task.Wait or Task.WaitAll) and cancellation is observed on a linked token. + /// Consumers should not treat this as a token-driven cancellation thrown by the runtime. + /// +#pragma warning disable RCS1194 // Implement exception constructors. + public class ServiceOperationCanceledException : OperationCanceledException +#pragma warning restore RCS1194 // Implement exception constructors. + { + internal ServiceOperationCanceledException(CancellationToken token) : base(token) { } + } +} diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index fe9be2df..38ed969a 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -43,8 +43,6 @@ private PolicyResult Fallback(Action action, TParam param, Actio result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { action(param); @@ -56,16 +54,11 @@ private PolicyResult Fallback(Action action, TParam param, Actio } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); - - if (!result.IsFailed) - { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); - } + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -85,8 +78,6 @@ private PolicyResult Fallback(Action action, Action fallback, result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { action(); @@ -98,16 +89,11 @@ private PolicyResult Fallback(Action action, Action fallback, } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); - - if (!result.IsFailed) - { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); - } + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -143,8 +129,6 @@ private PolicyResult Fallback(Func func, TParam param, result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(param); @@ -157,16 +141,11 @@ private PolicyResult Fallback(Func func, TParam param, } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); - - if (!result.IsFailed) - { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); - } + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -186,8 +165,6 @@ private PolicyResult Fallback(Func func, Func fal result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(); @@ -200,16 +177,11 @@ private PolicyResult Fallback(Func func, Func fal } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); - - if (!result.IsFailed) - { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); - } + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -255,8 +227,6 @@ private async Task FallbackAsync(Func FallbackAsync(Func FallbackAsync(Func fun result.SetExecuted(); - var exHandler = new SimpleAsyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), configureAwait, token); - try { await func(token).ConfigureAwait(configureAwait); @@ -306,12 +269,7 @@ private async Task FallbackAsync(Func fun } catch (Exception ex) { - await exHandler.HandleAsync(ex, emptyErrorContext).ConfigureAwait(configureAwait); - - if (!result.IsFailed) - { - (await fallback.HandleAsFallbackAsync(configureAwait, token).ConfigureAwait(configureAwait)).ChangePolicyResult(result, ex); - } + await HandleExceptionAsync(fallback, emptyErrorContext, result, ex, configureAwait, token).ConfigureAwait(configureAwait); } return result; } @@ -357,8 +315,6 @@ private async Task> FallbackAsync(Func> FallbackAsync(Func> FallbackAsync(Func> FallbackAsync(Func( + Func fallback, + EmptyErrorContext emptyErrorContext, + PolicyResult result, + Exception ex, + CancellationToken token) + { + bool policyRuleFunc(ErrorContext _, CancellationToken ct) + { + var fallbackResult = fallback(ct); + result.SetResult(fallbackResult); + return true; + } + + HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); + } + + private void HandleException( + Action fallback, + EmptyErrorContext emptyErrorContext, + PolicyResult result, + Exception ex, + CancellationToken token) + { + bool policyRuleFunc(ErrorContext _, CancellationToken ct) { fallback(ct); return true; } + + HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); + } + + private void HandleException( + Exception ex, + PolicyResult policyResult, + EmptyErrorContext errorContext, + Func, CancellationToken, bool> policyRuleFunc, + CancellationToken token) + + { + HandleException( + ex, + policyResult, + errorContext, + DefaultErrorSaver, + policyRuleFunc, + ExceptionHandlingBehavior.Handle, + ProcessingOrder.ProcessThenEvaluate, + ErrorProcessingCancellationEffect.Propagate, + token); + } + + private async Task HandleExceptionAsync( + Func> fallback, + EmptyErrorContext emptyErrorContext, + PolicyResult result, + Exception ex, + bool configureAwait, + CancellationToken token) + { + async Task policyRuleFunc(ErrorContext _, CancellationToken ct) + { + var fallbackResult = await fallback(ct).ConfigureAwait(configureAwait); + result.SetResult(fallbackResult); + return true; + } + + return await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + } + + private async Task HandleExceptionAsync( + Func fallback, + EmptyErrorContext emptyErrorContext, + PolicyResult result, + Exception ex, + bool configureAwait, + CancellationToken token) + { + async Task policyRuleFunc(ErrorContext _, CancellationToken ct) + { + await fallback(ct).ConfigureAwait(configureAwait); + return true; + } + + return await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + } + + private Task HandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + EmptyErrorContext errorContext, + Func, CancellationToken, Task> policyRuleFunc, + bool configAwait, + CancellationToken token) + + { + return HandleExceptionAsync( + ex, + policyResult, + errorContext, + DefaultAsyncErrorSaver, + policyRuleFunc, + ExceptionHandlingBehavior.Handle, + ProcessingOrder.ProcessThenEvaluate, + ErrorProcessingCancellationEffect.Propagate, + configAwait, + token); + } } } diff --git a/src/Fallback/FallbackFuncExecResult.cs b/src/Fallback/FallbackFuncExecResult.cs index e86f6253..3c7da60c 100644 --- a/src/Fallback/FallbackFuncExecResult.cs +++ b/src/Fallback/FallbackFuncExecResult.cs @@ -44,6 +44,9 @@ public static FallbackFuncExecResult FromCanceledError(OperationCanceledExceptio return new FallbackFuncExecResult() { IsCanceled = true, CanceledError = exception }; } +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("This method is obsolete")] +#pragma warning restore S1133 // Deprecated code should be removed public static FallbackFuncExecResult FromErrorAndToken(OperationCanceledException exception, CancellationToken token) { if (exception.CancellationToken.Equals(token)) @@ -52,6 +55,9 @@ public static FallbackFuncExecResult FromErrorAndToken(OperationCanceledExceptio return FromError(exception); } +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("This method is obsolete")] +#pragma warning restore S1133 // Deprecated code should be removed public static FallbackFuncExecResult FromErrorAndToken(AggregateException exception, CancellationToken token) { if (exception.HasCanceledException(token)) diff --git a/src/ICanAddErrorFilter.cs b/src/ICanAddErrorFilter.cs index b64ba365..4f0f3acf 100644 --- a/src/ICanAddErrorFilter.cs +++ b/src/ICanAddErrorFilter.cs @@ -22,4 +22,23 @@ public interface ICanAddErrorFilter where T : ICanAddErrorFilter /// T AddErrorFilter(Func filterFactory); } + + /// + /// Provides extension methods for the interface. + /// + public static class CanAddErrorFilterExtensions + { + /// + /// Wraps a into a + /// and adds it to the . + /// + /// A type implementing . + /// The instance this method extends. + /// The exception filter logic to be applied. + public static void AddExceptionFilter(this ICanAddErrorFilter filter, PolicyProcessor.ExceptionFilter errorFilter) where T : ICanAddErrorFilter + { + var neFilter = new NonEmptyCatchBlockFilter() { ErrorFilter = errorFilter }; + filter.AddErrorFilter(neFilter); + } + } } diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index a4b836b0..c08318dc 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Threading; +using System.Threading.Tasks; namespace PoliNorError { @@ -15,14 +16,14 @@ public abstract partial class PolicyProcessor : IPolicyProcessor #pragma warning restore S1133 // Deprecated code should be removed protected bool _isPolicyAliasSet; - protected PolicyProcessor(IBulkErrorProcessor bulkErrorProcessor = null): this(new ExceptionFilter(), bulkErrorProcessor) - {} + protected PolicyProcessor(IBulkErrorProcessor bulkErrorProcessor = null) : this(new ExceptionFilter(), bulkErrorProcessor) + { } #pragma warning disable S1133 // Deprecated code should be removed [Obsolete("This constructor is obsolete. Use constructors without the PolicyAlias parameter instead.")] #pragma warning restore S1133 // Deprecated code should be removed protected PolicyProcessor(PolicyAlias policyAlias, IBulkErrorProcessor bulkErrorProcessor = null) : this(policyAlias, new ExceptionFilter(), bulkErrorProcessor) - {} + { } protected PolicyProcessor(ExceptionFilter exceptionFilter, IBulkErrorProcessor bulkErrorProcessor = null) { @@ -55,7 +56,7 @@ public void AddErrorProcessor(IErrorProcessor newErrorProcessor) internal PolicyProcessorCatchBlockSyncHandler GetCatchBlockSyncHandler(PolicyResult policyResult, CancellationToken token, Func, bool> policyRuleFunc = null) { - return new PolicyProcessorCatchBlockSyncHandler (policyResult, + return new PolicyProcessorCatchBlockSyncHandler(policyResult, _bulkErrorProcessor, token, ErrorFilter.GetCanHandle(), @@ -72,7 +73,387 @@ internal PolicyProcessorCatchBlockAsyncHandler GetCatchBlockAsyncHandler(P policyRuleFunc); } + protected internal async Task HandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Func, bool, CancellationToken, Task> errorSaver, + Func, CancellationToken, Task> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + ProcessingOrder processingOrder, + ErrorProcessingCancellationEffect cancellationEffect, + bool configureAwait, + CancellationToken token) + { + var saver = errorSaver ?? CreateDefaultAsyncErrorSaver(); + if (handlingBehavior != ExceptionHandlingBehavior.ConditionalRethrow) + { + await saver(policyResult, ex, errorContext, configureAwait, token).ConfigureAwait(configureAwait); + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + } + + var (result, error) = EvaluateExceptionFilter(policyResult, ex, handlingBehavior); + + if (!(error is null) && handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + await saver(policyResult, ex, errorContext, configureAwait, token).ConfigureAwait(configureAwait); + } + + if (result != ExceptionHandlingResult.Accepted) + { + return result; + } + + if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + await saver(policyResult, ex, errorContext, configureAwait, token).ConfigureAwait(configureAwait); + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + } + + if (processingOrder == ProcessingOrder.EvaluateThenProcess) + { + var ruleResult = await EvaluatePolicyRuleAsync(ex, policyResult, errorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + if (ruleResult != ExceptionHandlingResult.Accepted) + { + return ruleResult; + } + + var bulkProcessResult = await _bulkErrorProcessor.ProcessAsync(ex, errorContext.ToProcessingErrorContext(), configureAwait, token).ConfigureAwait(configureAwait); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + } + return ExceptionHandlingResult.Handled; + } + else + { + var bulkProcessResult = await _bulkErrorProcessor.ProcessAsync(ex, errorContext.ToProcessingErrorContext(), configureAwait, token).ConfigureAwait(configureAwait); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + return await EvaluatePolicyRuleAsync(ex, policyResult, errorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + } + } + + protected internal ExceptionHandlingResult HandleException( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Action, CancellationToken> errorSaver, + Func, CancellationToken, bool> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + ProcessingOrder processingOrder, + ErrorProcessingCancellationEffect cancellationEffect, + CancellationToken token) + { + var saver = errorSaver ?? CreateDefaultErrorSaver(); + if (handlingBehavior != ExceptionHandlingBehavior.ConditionalRethrow) + { + saver(policyResult, ex, errorContext, token); + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + } + + var (result, error) = EvaluateExceptionFilter(policyResult, ex, handlingBehavior); + + if (!(error is null) && handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + saver(policyResult, ex, errorContext, token); + } + + if (result != ExceptionHandlingResult.Accepted) + { + return result; + } + + if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + saver(policyResult, ex, errorContext, token); + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + } + + if (processingOrder == ProcessingOrder.EvaluateThenProcess) + { + var ruleResult = EvaluatePolicyRule(ex, policyResult, errorContext, policyRuleFunc, token); + if (ruleResult != ExceptionHandlingResult.Accepted) + { + return ruleResult; + } + + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + } + + return ExceptionHandlingResult.Handled; + } + else + { + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + + return EvaluatePolicyRule(ex, policyResult, errorContext, policyRuleFunc, token); + } + } + + internal static Action, CancellationToken> DefaultErrorSaver { get; } = CreateDefaultErrorSaver(); + + internal static Action, CancellationToken> CreateDefaultErrorSaver() => + (pr, e, _, __) => pr.AddError(e); + + internal static Func, bool, CancellationToken, Task> DefaultAsyncErrorSaver { get; } = CreateDefaultAsyncErrorSaver(); + + internal static Func, bool, CancellationToken, Task> CreateDefaultAsyncErrorSaver() => + (pr, e, _, __, ___) => { pr.AddError(e); return Task.CompletedTask; }; + + internal static Func, CancellationToken, Task> DefaultAsyncPolicyRule { get; }= (_, __) => Task.FromResult(true); + + internal static Func, CancellationToken, bool> DefaultPolicyRule { get; } = CreateDefaultPolicyRule(); + + internal static Func, CancellationToken, bool> CreateDefaultPolicyRule() => + (_, __) => true; + + private (ExceptionHandlingResult, Exception) EvaluateExceptionFilter(PolicyResult policyResult, Exception ex, ExceptionHandlingBehavior handlingBehavior) + { + var (filterPassed, error) = RunErrorFilterFunc(); + return (FailPolicyResultIfRequired(), error); + ExceptionHandlingResult FailPolicyResultIfRequired() + { + if (!filterPassed) + { + switch (handlingBehavior) + { + case ExceptionHandlingBehavior.ConditionalRethrow when !(error is null): + policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.ErrorFilter, true)); + policyResult.SetFailedAndFilterUnsatisfied(); + return ExceptionHandlingResult.Handled; + case ExceptionHandlingBehavior.ConditionalRethrow: + return ExceptionHandlingResult.Rethrow; + case ExceptionHandlingBehavior.Handle: + if (!(error is null)) + { + policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.ErrorFilter, true)); + } + policyResult.SetFailedAndFilterUnsatisfied(); + return ExceptionHandlingResult.Handled; + default: + policyResult.AddCatchBlockError(new CatchBlockException(new NotSupportedException(), ex, CatchBlockExceptionSource.ErrorFilter, true)); + policyResult.SetFailedAndFilterUnsatisfied(); + return ExceptionHandlingResult.Handled; + } + } + return ExceptionHandlingResult.Accepted; + } + + (bool, Exception) RunErrorFilterFunc() + { + try + { + return (ErrorFilter.GetCanHandle()(ex), null); + } + catch (Exception exIn) + { + return (false, exIn); + } + } + } + + private static ExceptionHandlingResult EvaluatePolicyRule(Exception ex, PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, bool> policyRuleFunc, CancellationToken token) + { + return HandlePolicyRuleFuncResult(RunPolicyRuleFunc(), ex, policyResult); + + (bool Result, bool IsCanceled, Exception error) RunPolicyRuleFunc() + { + try + { + var result = policyRuleFunc?.Invoke(errorContext, token); + return (result != false, false, null); + } + catch (OperationCanceledException tce) when (token.IsCancellationRequested) + { + return (false, true, tce); + } + catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) + { + return (false, true, ae.GetCancellationException()); + } + catch (Exception cex) + { + return (false, false, cex); + } + } + } + + private static async Task EvaluatePolicyRuleAsync(Exception ex, PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, Task> policyRuleFunc, bool configureAwait, CancellationToken token) + { + return HandlePolicyRuleFuncResult(await RunPolicyRuleFunc().ConfigureAwait(configureAwait), ex, policyResult); + + async Task<(bool Result, bool IsCanceled, Exception error)> RunPolicyRuleFunc() + { + try + { + if (policyRuleFunc is null) + return (true, false, null); + var result = await policyRuleFunc(errorContext, token).ConfigureAwait(configureAwait); + return (result, false, null); + } + catch (OperationCanceledException tce) when (token.IsCancellationRequested) + { + return (false, true, tce); + } + catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) + { + return (false, true, ae.GetCancellationException()); + } + catch (Exception cex) + { + return (false, false, cex); + } + } + } + + private static ExceptionHandlingResult HandlePolicyRuleFuncResult((bool accepted, bool canceled, Exception error) result, Exception ex, PolicyResult policyResult) + { + if (result.accepted) + { + return ExceptionHandlingResult.Accepted; + } + else + { + if (!(result.error is null)) + { + if (result.canceled) + { + policyResult.SetFailedAndCanceled(); + policyResult.AddCatchBlockError(new CatchBlockException(result.error, ex, CatchBlockExceptionSource.PolicyRule)); + } + else + { + policyResult.SetFailedWithCatchBlockError(result.error, ex, CatchBlockExceptionSource.PolicyRule); + } + } + else + { + policyResult.SetFailedInner(); + } + + return ExceptionHandlingResult.Handled; + } + } + public IEnumerator GetEnumerator() => _bulkErrorProcessor.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + /// + /// Represents the outcome of an exception handling. + /// + public enum ExceptionHandlingResult + { + /// + /// The exception matched the configured filter and rule and should be processed. + /// + Accepted, + + /// + /// The exception was handled and will not be rethrown. + /// + Handled, + + /// + /// The exception was not handled and should be rethrown to the caller. + /// + Rethrow + } + + /// + /// Defines the desired behavior for an exception handling mechanism. + /// + public enum ExceptionHandlingBehavior + { + /// + /// Handle the exception and do not rethrow it. + /// Outcome: + /// or + /// + Handle, + + /// + /// Handle the exception and rethrow ONLY if an error filter condition is NOT satisfied. + /// Outcome: (if filtered) + /// or (if unfiltered) + /// + ConditionalRethrow + } + + /// + /// Describes whether a cancellation that occurred during should be propagated to . + /// + public enum ErrorProcessingCancellationEffect + { + /// + /// Cancellation during error processing does not influence + /// the policy execution result. + /// + /// + /// When this value is specified, cancellations occurring inside + /// error processors are treated as an internal execution concern + /// and remains false. + /// + Ignore, + + /// + /// Cancellation during error processing is propagated + /// to the policy execution result. + /// + /// + /// When this value is specified, a cancellation raised inside + /// an error processor causes + /// to be set to true. + /// + Propagate + } + + /// + /// Defines the execution order for policy rule evaluation and bulk error processing. + /// + public enum ProcessingOrder + { + /// + /// Evaluates the policy rule before bulk error processing. + /// + EvaluateThenProcess, + + /// + /// Performs bulk error processing before evaluating the policy rule. + /// + ProcessThenEvaluate + } } diff --git a/src/PolicyProcessorErrorFiltering.cs b/src/PolicyProcessorErrorFiltering.cs index 887e12bc..eab0a700 100644 --- a/src/PolicyProcessorErrorFiltering.cs +++ b/src/PolicyProcessorErrorFiltering.cs @@ -109,7 +109,7 @@ internal static void AddIncludedErrorSet(this IPolicyProcessor policyProcessor, { foreach (var item in errorSet.Items) { - policyProcessor.AddIncludedError(item); + policyProcessor.ErrorFilter.AddIncludedError(item); } } @@ -124,7 +124,7 @@ internal static void AddExcludedErrorSet(this IPolicyProcessor policyProcessor, { foreach (var item in errorSet.Items) { - policyProcessor.AddExcludedError(item); + policyProcessor.ErrorFilter.AddExcludedError(item); } } @@ -137,15 +137,5 @@ internal static void AddExcludedInnerErrorFilter(this IPolicyPr { policyProcessor.ErrorFilter.AddExcludedInnerErrorFilter(func); } - - internal static void AddIncludedError(this IPolicyProcessor policyProcessor, ErrorSetItem errorSetItem) - { - policyProcessor.ErrorFilter.AddIncludedError(errorSetItem); - } - - internal static void AddExcludedError(this IPolicyProcessor policyProcessor, ErrorSetItem errorSetItem) - { - policyProcessor.ErrorFilter.AddExcludedError(errorSetItem); - } } } diff --git a/src/PolicyResult.cs b/src/PolicyResult.cs index 651ef708..b1dc238d 100644 --- a/src/PolicyResult.cs +++ b/src/PolicyResult.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace PoliNorError { @@ -17,8 +18,13 @@ public class PolicyResult internal bool _executed; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static PolicyResult ForSync() => new PolicyResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static PolicyResult ForNotSync() => new PolicyResult(true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static PolicyResult InitByConfigureAwait(bool configureAwait) => !configureAwait ? ForNotSync() : ForSync(); /// @@ -306,8 +312,13 @@ internal virtual PolicyResult GetLastWrappedPolicyResult() /// Type of result. public class PolicyResult : PolicyResult { + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static new PolicyResult ForSync() => new PolicyResult(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static new PolicyResult ForNotSync() => new PolicyResult(true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static new PolicyResult InitByConfigureAwait(bool configureAwait) => !configureAwait ? ForNotSync() : ForSync(); /// diff --git a/src/PolicyResultExtensions.cs b/src/PolicyResultExtensions.cs index 40a650e9..56fe83ec 100644 --- a/src/PolicyResultExtensions.cs +++ b/src/PolicyResultExtensions.cs @@ -10,7 +10,10 @@ internal static class PolicyResultExtensions { internal static void AddBulkProcessorErrors(this PolicyResult policyResult, BulkErrorProcessor.BulkProcessResult bulkProcessResult) { - policyResult.AddCatchBlockErrors(bulkProcessResult.ToCatchBlockExceptions()); + if (bulkProcessResult.HasProcessErrors) + { + policyResult.AddCatchBlockErrors(bulkProcessResult.ToCatchBlockExceptions()); + } } internal static bool WasResultSetToFailureByCatchBlock(this PolicyResult policyResult, HandleCatchBlockResult canHandleResult) diff --git a/src/Retry/ConstantRetryDelay.cs b/src/Retry/ConstantRetryDelay.cs index 3102a601..6fc78f3d 100644 --- a/src/Retry/ConstantRetryDelay.cs +++ b/src/Retry/ConstantRetryDelay.cs @@ -11,7 +11,7 @@ public sealed class ConstantRetryDelay : RetryDelay /// Initializes a new instance of . /// /// - public ConstantRetryDelay(ConstantRetryDelayOptions retryDelayOptions) : base(GetDelayValueProvider(retryDelayOptions)) + public ConstantRetryDelay(ConstantRetryDelayOptions retryDelayOptions) : base(new ConstantDelayCore(retryDelayOptions)) { if (retryDelayOptions.UseJitter && retryDelayOptions.MaxDelay < retryDelayOptions.BaseDelay) { @@ -22,18 +22,6 @@ public ConstantRetryDelay(ConstantRetryDelayOptions retryDelayOptions) : base(Ge #pragma warning restore CS0618 // Type or member is obsolete } - private static Func GetDelayValueProvider(ConstantRetryDelayOptions retryDelayOptions) - { - if (retryDelayOptions.UseJitter) - { - return GetJitteredDelayValue(retryDelayOptions); - } - else - { - return GetDelayValue(retryDelayOptions); - } - } - /// /// Creates . /// @@ -44,29 +32,40 @@ private static Func GetDelayValueProvider(ConstantRetryDelayOptio public static ConstantRetryDelay Create(TimeSpan baseDelay, TimeSpan? maxDelay = null, bool useJitter = false) => new ConstantRetryDelay(baseDelay, maxDelay, useJitter); internal ConstantRetryDelay(TimeSpan baseDelay, TimeSpan? maxDelay = null, bool useJitter = false) : this(new ConstantRetryDelayOptions() { BaseDelay = baseDelay, UseJitter = useJitter, MaxDelay = maxDelay ?? TimeSpan.MaxValue }){} + } + + /// + /// Represents options for the . + /// + public class ConstantRetryDelayOptions : RetryDelayOptions + { + public override RetryDelayType DelayType => RetryDelayType.Constant; + + public static implicit operator ConstantRetryDelay(ConstantRetryDelayOptions options) => new ConstantRetryDelay(options); + } - private static Func GetDelayValue(ConstantRetryDelayOptions options) => (_) => options.BaseDelay; + internal class ConstantDelayCore : DelayCoreBase + { + private readonly ConstantRetryDelayOptions _delayOptions; + private readonly MaxDelayDelimiter _maxDelayDelimiter; - private static Func GetJitteredDelayValue(ConstantRetryDelayOptions options) + public ConstantDelayCore(ConstantRetryDelayOptions delayOptions) : base(delayOptions) { - return (_) => + _delayOptions = delayOptions; + if (delayOptions.UseJitter) { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(ApplyJitter(GetDelayValueInMs(options))); - }; + _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); + } } - private static double GetDelayValueInMs(ConstantRetryDelayOptions options) + protected override TimeSpan GetBaseDelay(int attempt) { - return options.BaseDelay.TotalMilliseconds; + return _delayOptions.BaseDelay; } - } - /// - /// Represents options for the . - /// - public class ConstantRetryDelayOptions : RetryDelayOptions - { - public override RetryDelayType DelayType => RetryDelayType.Constant; + protected override TimeSpan GetJitteredDelay(int attempt) + { + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(StandardJitter.AddJitter(_delayOptions.BaseDelay.TotalMilliseconds)); + } } } diff --git a/src/Retry/DelayCoreBase.cs b/src/Retry/DelayCoreBase.cs new file mode 100644 index 00000000..101b6e77 --- /dev/null +++ b/src/Retry/DelayCoreBase.cs @@ -0,0 +1,25 @@ +using System; + +namespace PoliNorError +{ + internal abstract class DelayCoreBase + { + protected DelayCoreBase(RetryDelayOptions delayOptions) + { + if (delayOptions.UseJitter) + { + DelayProvider = GetJitteredDelay; + } + else + { + DelayProvider = GetBaseDelay; + } + } + + public Func DelayProvider { get; } + + protected abstract TimeSpan GetBaseDelay(int attempt); + + protected abstract TimeSpan GetJitteredDelay(int attempt); + } +} diff --git a/src/Retry/ExponentialRetryDelay.cs b/src/Retry/ExponentialRetryDelay.cs index 58c14aeb..4eaf2dee 100644 --- a/src/Retry/ExponentialRetryDelay.cs +++ b/src/Retry/ExponentialRetryDelay.cs @@ -1,4 +1,5 @@ using System; +using static PoliNorError.ExponentialRetryDelay; namespace PoliNorError { @@ -11,43 +12,13 @@ public sealed partial class ExponentialRetryDelay : RetryDelay /// Initializes a new instance of . /// /// - public ExponentialRetryDelay(ExponentialRetryDelayOptions retryDelayOptions) : base(GetDelayValueProvider(retryDelayOptions)) + public ExponentialRetryDelay(ExponentialRetryDelayOptions retryDelayOptions) : base(new ExponentialDelayCore(retryDelayOptions)) { #pragma warning disable CS0618 // Type or member is obsolete InnerDelay = this; #pragma warning restore CS0618 // Type or member is obsolete } - private static Func GetDelayValueProvider(ExponentialRetryDelayOptions retryDelayOptions) - { - if (retryDelayOptions.UseJitter) - { - return GetJitteredDelayValue(retryDelayOptions); - } - else - { - return GetDelayValue(retryDelayOptions); - } - } - - private static Func GetJitteredDelayValue(ExponentialRetryDelayOptions options) - { - return (attempt) => - { - var dj = new DecorrelatedJitter(options.BaseDelay, options.ExponentialFactor, options.MaxDelay); - return dj.DecorrelatedJitterBackoffV2(attempt); - }; - } - - private static Func GetDelayValue(ExponentialRetryDelayOptions options) - { - return (attempt) => - { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(Math.Pow(options.ExponentialFactor, attempt) * options.BaseDelay.TotalMilliseconds); - }; - } - /// /// Creates . /// @@ -82,5 +53,35 @@ public class ExponentialRetryDelayOptions : RetryDelayOptions /// Exponential factor to use. /// public double ExponentialFactor { get; set; } = RetryDelayConstants.ExponentialFactor; + + public static implicit operator ExponentialRetryDelay(ExponentialRetryDelayOptions options) => new ExponentialRetryDelay(options); + } + + internal class ExponentialDelayCore : DelayCoreBase + { + private readonly ExponentialRetryDelayOptions _delayOptions; + + private readonly DecorrelatedJitter _jitter; + private readonly MaxDelayDelimiter _maxDelayDelimiter; + + public ExponentialDelayCore(ExponentialRetryDelayOptions delayOptions) : base(delayOptions) + { + _delayOptions = delayOptions; + + if (delayOptions.UseJitter) + { + _jitter = new DecorrelatedJitter(delayOptions.BaseDelay, delayOptions.ExponentialFactor, delayOptions.MaxDelay); + } + else + { + _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); + } + } + + protected override TimeSpan GetBaseDelay(int attempt) + => _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(Math.Pow(_delayOptions.ExponentialFactor, attempt) * _delayOptions.BaseDelay.TotalMilliseconds); + + protected override TimeSpan GetJitteredDelay(int attempt) + => _jitter.DecorrelatedJitterBackoffV2(attempt); } } diff --git a/src/Retry/LinearRetryDelay.cs b/src/Retry/LinearRetryDelay.cs index 526223eb..cef7dc9a 100644 --- a/src/Retry/LinearRetryDelay.cs +++ b/src/Retry/LinearRetryDelay.cs @@ -11,25 +11,12 @@ public sealed class LinearRetryDelay : RetryDelay /// Initializes a new instance of . /// /// - public LinearRetryDelay(LinearRetryDelayOptions retryDelayOptions): base(GetDelayValueProvider(retryDelayOptions)) + public LinearRetryDelay(LinearRetryDelayOptions retryDelayOptions): base(new LinearDelayCore(retryDelayOptions)) { #pragma warning disable CS0618 // Type or member is obsolete InnerDelay = this; #pragma warning restore CS0618 // Type or member is obsolete } - - private static Func GetDelayValueProvider(LinearRetryDelayOptions retryDelayOptions) - { - if (retryDelayOptions.UseJitter) - { - return GetJitteredDelayValue(retryDelayOptions); - } - else - { - return GetDelayValue(retryDelayOptions); - } - } - /// /// Creates . /// @@ -51,29 +38,6 @@ private static Func GetDelayValueProvider(LinearRetryDelayOptions internal LinearRetryDelay(TimeSpan baseDelay, double slopeFactor = RetryDelayConstants.SlopeFactor, TimeSpan? maxDelay = null, bool useJitter = false) : this(new LinearRetryDelayOptions() { BaseDelay = baseDelay, SlopeFactor = slopeFactor, UseJitter = useJitter, MaxDelay = maxDelay ?? TimeSpan.MaxValue } ) {} - - private static Func GetDelayValue(LinearRetryDelayOptions options) - { - return (attempt) => - { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(GetDelayValueInMs(attempt, options)); - }; - } - - private static Func GetJitteredDelayValue(LinearRetryDelayOptions options) - { - return (attempt) => - { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(ApplyJitter(GetDelayValueInMs(attempt, options))); - }; - } - - private static double GetDelayValueInMs(int attempt, LinearRetryDelayOptions options) - { - return (attempt + 1) * options.SlopeFactor * options.BaseDelay.TotalMilliseconds; - } } /// @@ -87,5 +51,34 @@ public class LinearRetryDelayOptions : RetryDelayOptions /// Slope factor to use. /// public double SlopeFactor { get; set; } = RetryDelayConstants.SlopeFactor; + + public static implicit operator LinearRetryDelay(LinearRetryDelayOptions options) => new LinearRetryDelay(options); + } + + internal class LinearDelayCore : DelayCoreBase + { + private readonly LinearRetryDelayOptions _delayOptions; + private readonly MaxDelayDelimiter _maxDelayDelimiter; + + public LinearDelayCore(LinearRetryDelayOptions delayOptions) : base(delayOptions) + { + _delayOptions = delayOptions; + _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); + } + + protected override TimeSpan GetBaseDelay(int attempt) + { + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(GetDelayValueInMs(attempt, _delayOptions)); + } + + protected override TimeSpan GetJitteredDelay(int attempt) + { + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(StandardJitter.AddJitter(GetDelayValueInMs(attempt, _delayOptions))); + } + + private static double GetDelayValueInMs(int attempt, LinearRetryDelayOptions options) + { + return (attempt + 1) * options.SlopeFactor * options.BaseDelay.TotalMilliseconds; + } } } diff --git a/src/Retry/RetryDelay.cs b/src/Retry/RetryDelay.cs index cd9c3654..cd109bcc 100644 --- a/src/Retry/RetryDelay.cs +++ b/src/Retry/RetryDelay.cs @@ -27,6 +27,8 @@ protected RetryDelay() { } + internal RetryDelay(DelayCoreBase delayCore) : this(delayCore.DelayProvider) { } + /// /// Initializes a new instance of . /// diff --git a/src/Retry/StandardJitter.cs b/src/Retry/StandardJitter.cs new file mode 100644 index 00000000..7754e4fd --- /dev/null +++ b/src/Retry/StandardJitter.cs @@ -0,0 +1,12 @@ +namespace PoliNorError +{ + internal static class StandardJitter + { + internal static double AddJitter(double delayInMs) + { + var offset = (delayInMs * RetryDelayConstants.JitterFactor) / 2; + var randomDelay = (delayInMs * RetryDelayConstants.JitterFactor * StaticRandom.RandDouble()) - offset; + return delayInMs + randomDelay; + } + } +} diff --git a/src/Retry/TimeSeriesRetryDelay.cs b/src/Retry/TimeSeriesRetryDelay.cs index 9932ce43..daf3b86f 100644 --- a/src/Retry/TimeSeriesRetryDelay.cs +++ b/src/Retry/TimeSeriesRetryDelay.cs @@ -12,8 +12,8 @@ public sealed class TimeSeriesRetryDelay : RetryDelay /// /// Initializes a new instance of . /// - /// - public TimeSeriesRetryDelay(TimeSeriesRetryDelayOptions timeSeriesOptions) : base(GetDelayValueProvider(timeSeriesOptions)){} + /// + public TimeSeriesRetryDelay(TimeSeriesRetryDelayOptions timeSeriesOptions) : base(new TimeSeriesDelayCore(timeSeriesOptions)) {} /// /// Initializes a new instance of the class with the specified base delay, optional maximum delay, and jitter setting. @@ -94,50 +94,41 @@ public static TimeSeriesRetryDelay Create(IEnumerable times, TimeSpan? Times = times.ToArray() }); } + } + + internal class TimeSeriesDelayCore : DelayCoreBase + { + private readonly MaxDelayDelimiter _maxDelayDelimiter; + private readonly TimeSpan[] _times; + private readonly int _maxIndex; - private static Func GetDelayValueProvider(TimeSeriesRetryDelayOptions retryDelayOptions) + public TimeSeriesDelayCore(TimeSeriesRetryDelayOptions delayOptions) : base(delayOptions) { - TimeSpan[] times; - if (retryDelayOptions.Times?.Length == 0) - { - times = new[] { retryDelayOptions.BaseDelay > retryDelayOptions.MaxDelay ? retryDelayOptions.MaxDelay : retryDelayOptions.BaseDelay }; - } - else - { - times = retryDelayOptions.Times; - } - if (retryDelayOptions.UseJitter) + _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); + if (delayOptions.Times?.Length == 0) { - return GetJitteredDelayValueFunc(retryDelayOptions, times); + _times = new[] { delayOptions.BaseDelay > delayOptions.MaxDelay ? delayOptions.MaxDelay : delayOptions.BaseDelay }; } else { - return GetDelayValueFunc(retryDelayOptions, times); + _times = delayOptions.Times; } + _maxIndex = _times.Length - 1; } - private static Func GetDelayValueFunc(RetryDelayOptions options, TimeSpan[] timeSpans) + protected override TimeSpan GetBaseDelay(int attempt) { - return (attempt) => - { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(GetDelayInner(attempt, timeSpans).TotalMilliseconds); - }; + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(GetDelayInner(attempt).TotalMilliseconds); } - private static Func GetJitteredDelayValueFunc(RetryDelayOptions options, TimeSpan[] timeSpans) + protected override TimeSpan GetJitteredDelay(int attempt) { - return (attempt) => - { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(ApplyJitter(GetDelayInner(attempt, timeSpans).TotalMilliseconds)); - }; + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(StandardJitter.AddJitter(GetDelayInner(attempt).TotalMilliseconds)); } - private static TimeSpan GetDelayInner(int attempt, TimeSpan[] times) + private TimeSpan GetDelayInner(int attempt) { - int maxIndex = times.Length - 1; - return times[(uint)attempt <= (uint)maxIndex ? attempt : maxIndex]; + return _times[(uint)attempt <= (uint)_maxIndex ? attempt : _maxIndex]; } } } diff --git a/src/Retry/TimeSeriesRetryDelayOptions.cs b/src/Retry/TimeSeriesRetryDelayOptions.cs index 007329ff..8c4abdd7 100644 --- a/src/Retry/TimeSeriesRetryDelayOptions.cs +++ b/src/Retry/TimeSeriesRetryDelayOptions.cs @@ -14,5 +14,7 @@ public class TimeSeriesRetryDelayOptions : RetryDelayOptions /// public override RetryDelayType DelayType => RetryDelayType.TimeSeries; + + public static implicit operator TimeSeriesRetryDelay(TimeSeriesRetryDelayOptions options) => new TimeSeriesRetryDelay(options); } } diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 6f05b264..635f6f74 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -22,10 +22,10 @@ public SimplePolicyProcessor(bool rethrowIfErrorFilterUnsatisfied = false) : thi /// /// Specifies whether an exception is rethrown if the error filter is unsatisfied. public SimplePolicyProcessor(IBulkErrorProcessor bulkErrorProcessor, bool rethrowIfErrorFilterUnsatisfied = false) : this(bulkErrorProcessor, null, rethrowIfErrorFilterUnsatisfied) - {} + { } internal SimplePolicyProcessor(CatchBlockFilter catchBlockFilter, IBulkErrorProcessor bulkErrorProcessor = null, bool rethrowIfErrorFilterUnsatisfied = false) : this(bulkErrorProcessor, (catchBlockFilter ?? new CatchBlockFilter()).ErrorFilter, rethrowIfErrorFilterUnsatisfied) - {} + { } private SimplePolicyProcessor(IBulkErrorProcessor bulkErrorProcessor, ExceptionFilter exceptionFilter, bool rethrowIfErrorFilterUnsatisfied) : base(exceptionFilter ?? new ExceptionFilter(), bulkErrorProcessor) { @@ -76,8 +76,6 @@ private PolicyResult Execute(Action action, TParam param, EmptyE result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { action(param); @@ -89,26 +87,16 @@ private PolicyResult Execute(Action action, TParam param, EmptyE } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var handlingResult = HandleException(ex, result, emptyErrorContext, token); + if (handlingResult == ExceptionHandlingResult.Rethrow) { - var (filterUnsatisfied, filterException) = GetFilterUnsatisfiedOrFilterException(ex); - if (filterUnsatisfied == true) - { - ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; - throw; - } - else if (!(filterException is null)) - { - result.AddErrorAndCatchBlockFilterError(ex, filterException); - return result; - } + ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; + throw; } - - exHandler.Handle(ex, emptyErrorContext); } return result; } @@ -128,8 +116,6 @@ private PolicyResult Execute(Action action, EmptyErrorContext emptyErrorContext, result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { action(); @@ -141,26 +127,16 @@ private PolicyResult Execute(Action action, EmptyErrorContext emptyErrorContext, } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var handlingResult = HandleException(ex, result, emptyErrorContext, token); + if (handlingResult == ExceptionHandlingResult.Rethrow) { - var (filterUnsatisfied, filterException) = GetFilterUnsatisfiedOrFilterException(ex); - if (filterUnsatisfied == true) - { - ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; - throw; - } - else if (!(filterException is null)) - { - result.AddErrorAndCatchBlockFilterError(ex, filterException); - return result; - } + ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; + throw; } - - exHandler.Handle(ex, emptyErrorContext); } return result; } @@ -197,8 +173,6 @@ private PolicyResult Execute(Func func, TParam param, E result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(param); @@ -211,26 +185,16 @@ private PolicyResult Execute(Func func, TParam param, E } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var handlingResult = HandleException(ex, result, emptyErrorContext, token); + if (handlingResult == ExceptionHandlingResult.Rethrow) { - var (filterUnsatisfied, filterException) = GetFilterUnsatisfiedOrFilterException(ex); - if (filterUnsatisfied == true) - { - ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; - throw; - } - else if (!(filterException is null)) - { - result.AddErrorAndCatchBlockFilterError(ex, filterException); - return result; - } + ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; + throw; } - - exHandler.Handle(ex, emptyErrorContext); } return result; } @@ -250,8 +214,6 @@ private PolicyResult Execute(Func func, EmptyErrorContext emptyErrorCon result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(); @@ -264,26 +226,16 @@ private PolicyResult Execute(Func func, EmptyErrorContext emptyErrorCon } catch (AggregateException ae) when (ae.IsOperationCanceledWithRequestedToken(token)) { - result.SetFailedAndCanceled(ae.GetCancellationException()); + result.SetFailedAndCanceled(ae.GetCancellationException(token)); } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var handlingResult = HandleException(ex, result, emptyErrorContext, token); + if (handlingResult == ExceptionHandlingResult.Rethrow) { - var (filterUnsatisfied, filterException) = GetFilterUnsatisfiedOrFilterException(ex); - if (filterUnsatisfied == true) - { - ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; - throw; - } - else if (!(filterException is null)) - { - result.AddErrorAndCatchBlockFilterError(ex, filterException); - return result; - } + ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; + throw; } - - exHandler.Handle(ex, emptyErrorContext); } return result; } @@ -330,8 +282,6 @@ private async Task ExecuteAsync(Func ExecuteAsync(Func ExecuteAsync(Func func result.SetExecuted(); - var exHandler = new SimpleAsyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), configureAwait, token); - try { await func(token).ConfigureAwait(configureAwait); @@ -391,22 +329,12 @@ private async Task ExecuteAsync(Func func } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var handlingResult = await HandleExceptionAsync(ex, result, emptyErrorContext, configureAwait, token).ConfigureAwait(configureAwait); + if (handlingResult == ExceptionHandlingResult.Rethrow) { - var (filterUnsatisfied, filterException) = GetFilterUnsatisfiedOrFilterException(ex); - if (filterUnsatisfied == true) - { - ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; - throw; - } - else if (!(filterException is null)) - { - result.AddErrorAndCatchBlockFilterError(ex, filterException); - return result; - } + ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; + throw; } - - await exHandler.HandleAsync(ex, emptyErrorContext).ConfigureAwait(configureAwait); } return result; } @@ -453,8 +381,6 @@ private async Task> ExecuteAsync(Func> ExecuteAsync(Func> ExecuteAsync(Func> ExecuteAsync(Func(DefaultErr return this.WithErrorContextProcessor(errorProcessor); } - private (bool? FilterUnsatisfied, Exception exception) GetFilterUnsatisfiedOrFilterException(Exception ex) - { - try - { - return (!ErrorFilter.GetCanHandle()(ex), null); - } - catch (Exception filterEx) - { - return (null, filterEx); - } - } - /// public SimplePolicyProcessor AddErrorFilter(NonEmptyCatchBlockFilter filter) { @@ -596,5 +488,45 @@ public SimplePolicyProcessor AddErrorFilter(Func HandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + EmptyErrorContext errorContext, + bool configAwait, + CancellationToken token) + + { + return HandleExceptionAsync( + ex, + policyResult, + errorContext, + DefaultAsyncErrorSaver, + DefaultAsyncPolicyRule, + _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, + ProcessingOrder.EvaluateThenProcess, + ErrorProcessingCancellationEffect.Propagate, + configAwait, + token); + } } } diff --git a/src/Utilities/ExceptionExtensions.cs b/src/Utilities/ExceptionExtensions.cs index 4370ef11..dd0bb080 100644 --- a/src/Utilities/ExceptionExtensions.cs +++ b/src/Utilities/ExceptionExtensions.cs @@ -6,20 +6,20 @@ namespace PoliNorError { internal static class ExceptionExtensions { - public static bool IsOperationCanceledWithRequestedToken(this AggregateException ae, CancellationToken token) => ae.Flatten().InnerExceptions - .Any(ie => ie is OperationCanceledException && token.IsCancellationRequested); + public static bool IsOperationCanceledWithRequestedToken(this AggregateException _, + CancellationToken token) => token.IsCancellationRequested; public static bool HasCanceledException(this AggregateException ae, CancellationToken token) => ae.Flatten().InnerExceptions .Any(ie => ie is OperationCanceledException operationCanceledException && operationCanceledException.CancellationToken.Equals(token)); - public static OperationCanceledException GetCancellationException(this AggregateException aggregateException) + public static OperationCanceledException GetCancellationException(this AggregateException aggregateException, CancellationToken token = default) { - return aggregateException.Flatten() + var resExc = aggregateException.Flatten() .InnerExceptions .OfType() - .FirstOrDefault() - ?? - new OperationCanceledException(); + .FirstOrDefault(ex => ex.CancellationToken.Equals(token)); + + return resExc ?? new ServiceOperationCanceledException(token); } internal static bool DataContainsKeyStringWithValue(this Exception exception, string key, TValue value) diff --git a/src/Utilities/FuncEntensions.cs b/src/Utilities/FuncEntensions.cs index cb1ad8ce..29f638c7 100644 --- a/src/Utilities/FuncEntensions.cs +++ b/src/Utilities/FuncEntensions.cs @@ -6,11 +6,11 @@ namespace PoliNorError { internal static class FuncEntensions { - public static Func ToTaskReturnFunc(this Action action) + public static Func ToTaskReturnFunc(this Action action, bool propagateCancellation = true) { return (ct) => { - if (ct.IsCancellationRequested) + if (ct.IsCancellationRequested && propagateCancellation) { return Task.FromCanceled(ct); } @@ -19,11 +19,11 @@ public static Func ToTaskReturnFunc(this Action> ToTaskReturnFunc(this Func func) + public static Func> ToTaskReturnFunc(this Func func, bool propagateCancellation = true) { return (ct) => { - if (ct.IsCancellationRequested) + if (ct.IsCancellationRequested && propagateCancellation) { return Task.FromCanceled(ct); } @@ -31,30 +31,11 @@ public static Func> ToTaskReturnFunc(this Func ToAsyncFunc(this Action action) - { - return (ct) => - { - if (ct.IsCancellationRequested) - { - return Task.FromCanceled(ct); - } - action(ct); - return Task.CompletedTask; - }; - } - - public static Func> ToAsyncFunc(this Func func) - { - return (ct) => - { - if (ct.IsCancellationRequested) - { - return Task.FromCanceled(ct); - } - return Task.FromResult(func(ct)); - }; - } + public static Func ToAsyncFunc(this Action action) + => action.ToTaskReturnFunc(); + + public static Func> ToAsyncFunc(this Func func) + => func.ToTaskReturnFunc(); public static Action ToSyncFunc(this Func func) { @@ -167,10 +148,10 @@ public static Func ToCancelableFunc(this Func fnTask(t).WithCancellation(ct); } - public static Func ToCancelableFunc(this Func fnTask) - { - return (t, k, ct) => fnTask(t,k).WithCancellation(ct); - } + public static Func ToCancelableFunc(this Func fnTask) + { + return (t, k, ct) => fnTask(t, k).WithCancellation(ct); + } public static Func ToCancelableFunc(this Func fallbackAsync, CancellationType convertType, bool throwIfCanceled = false) { diff --git a/src/Wrap/PolicyWrapper.T.cs b/src/Wrap/PolicyWrapper.T.cs index 68f4e42a..e681eba8 100644 --- a/src/Wrap/PolicyWrapper.T.cs +++ b/src/Wrap/PolicyWrapper.T.cs @@ -28,9 +28,6 @@ private protected PolicyWrapper(Func func, CancellationToken token) : base(to internal abstract Task HandleAsync(CancellationToken token); - internal IEnumerable> PolicyDelegateResults - { - get { return _policyHandledResults; } - } + internal IEnumerable> PolicyDelegateResults => _policyHandledResults; } } diff --git a/src/Wrap/PolicyWrapper.cs b/src/Wrap/PolicyWrapper.cs index d617eebf..7a8681dd 100644 --- a/src/Wrap/PolicyWrapper.cs +++ b/src/Wrap/PolicyWrapper.cs @@ -3,9 +3,9 @@ using System.Threading; using System.Threading.Tasks; -namespace PoliNorError -{ - internal abstract class PolicyWrapper : PolicyWrapperBase +namespace PoliNorError +{ + internal abstract class PolicyWrapper : PolicyWrapperBase { protected readonly Func _func; protected readonly Action _action; @@ -27,9 +27,6 @@ private protected PolicyWrapper(Action action, CancellationToken token) : base(t internal abstract void Handle(); - internal IEnumerable PolicyDelegateResults - { - get { return _policyHandledResults; } - } - } -} + internal IEnumerable PolicyDelegateResults => _policyHandledResults; + } +} diff --git a/tests/App.config b/tests/App.config index 6fac0f17..61ac0305 100644 --- a/tests/App.config +++ b/tests/App.config @@ -17,11 +17,11 @@ - + - + diff --git a/tests/BulkProcessResultTests.cs b/tests/BulkProcessResultTests.cs new file mode 100644 index 00000000..8c0d097d --- /dev/null +++ b/tests/BulkProcessResultTests.cs @@ -0,0 +1,205 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using static PoliNorError.BulkErrorProcessor; + +namespace PoliNorError.Tests +{ + [TestFixture] + public class BulkProcessResultTests + { + [Test] + public void Should_InitializeWithHandlingError() + { + // Arrange + var handlingError = new Exception("Test error"); + + // Act + var result = new BulkProcessResult(handlingError, null); + + // Assert + Assert.That(result.HandlingError, Is.EqualTo(handlingError)); + } + + [Test] + public void Should_InitializeWithEmptyProcessErrorsWhenNull() + { + // Arrange + var handlingError = new Exception("Test error"); + + // Act + var result = new BulkProcessResult(handlingError, null); + + // Assert + Assert.That(result.ProcessErrors, Is.Empty); + } + + [Test] + public void Should_InitializeWithProvidedProcessErrors() + { + // Arrange + var handlingError = new Exception("Test error"); + var processErrors = new List + { + new ErrorProcessorException(new Exception("Error 1"), null, ProcessStatus.Faulted), + new ErrorProcessorException(new Exception("Error 2"), null, ProcessStatus.Faulted) + }; + + // Act + var result = new BulkProcessResult(handlingError, processErrors); + + // Assert + Assert.That(result.ProcessErrors, Has.Count.EqualTo(2)); + Assert.That(result.ProcessErrors, Is.EquivalentTo(processErrors)); + } + + [Test] + public void Should_ReturnFalseForHasProcessErrorsWhenNoErrors() + { + // Arrange + var handlingError = new Exception("Test error"); + + // Act + var result = new BulkProcessResult(handlingError, null); + + // Assert + Assert.That(result.HasProcessErrors, Is.False); + } + + [Test] + public void Should_ReturnTrueForHasProcessErrorsWhenErrorsExist() + { + // Arrange + var handlingError = new Exception("Test error"); + var processErrors = new List + { + new ErrorProcessorException(new Exception("Error 1"), null, ProcessStatus.Faulted) + }; + + // Act + var result = new BulkProcessResult(handlingError, processErrors); + + // Assert + Assert.That(result.HasProcessErrors, Is.True); + } + + [Test] + public void Should_ReturnFalseForIsCanceledWhenNoErrorsAndNotCanceledBetweenProcessOne() + { + // Arrange + var handlingError = new Exception("Test error"); + + // Act + var result = new BulkProcessResult(handlingError, null, false); + + // Assert + Assert.That(result.IsCanceled, Is.False); + } + + [Test] + public void Should_ReturnTrueForIsCanceledWhenCanceledBetweenProcessOne() + { + // Arrange + var handlingError = new Exception("Test error"); + + // Act + var result = new BulkProcessResult(handlingError, null, true); + + // Assert + Assert.That(result.IsCanceled, Is.True); + } + + [Test] + public void Should_ReturnTrueForIsCanceledWhenProcessErrorHasCanceledStatus() + { + // Arrange + var handlingError = new Exception("Test error"); + var processErrors = new List + { + new ErrorProcessorException(new Exception("Error 1"), null, ProcessStatus.Canceled) + }; + + // Act + var result = new BulkProcessResult(handlingError, processErrors, false); + + // Assert + Assert.That(result.IsCanceled, Is.True); + } + + [Test] + public void Should_ReturnFalseForIsCanceledWhenProcessErrorsHaveNonCanceledStatus() + { + // Arrange + var handlingError = new Exception("Test error"); + var processErrors = new List + { + new ErrorProcessorException(new Exception("Error 1"), null, ProcessStatus.Faulted), + new ErrorProcessorException(new Exception("Error 2"), null, ProcessStatus.Faulted) + }; + + // Act + var result = new BulkProcessResult(handlingError, processErrors, false); + + // Assert + Assert.That(result.IsCanceled, Is.False); + } + + [Test] + public void Should_ConvertProcessErrorsToCatchBlockExceptions() + { + // Arrange + var handlingError = new Exception("Test error"); + var processErrors = new List + { + new ErrorProcessorException(new Exception("Error 1"), null, ProcessStatus.Faulted), + new ErrorProcessorException(new Exception("Error 2"), null, ProcessStatus.Faulted) + }; + var result = new BulkProcessResult(handlingError, processErrors); + + // Act + var catchBlockExceptions = result.ToCatchBlockExceptions().ToList(); + + // Assert + Assert.That(catchBlockExceptions, Has.Count.EqualTo(2)); + Assert.That(catchBlockExceptions[0].InnerException, Is.EqualTo(handlingError)); + Assert.That(catchBlockExceptions[1].InnerException, Is.EqualTo(handlingError)); + + Assert.That(catchBlockExceptions[0].ProcessingException, Is.EqualTo(processErrors[0])); + Assert.That(catchBlockExceptions[1].ProcessingException, Is.EqualTo(processErrors[1])); + } + + [Test] + public void Should_ReturnEmptyEnumerableFromToCatchBlockExceptionsWhenNoProcessErrors() + { + // Arrange + var handlingError = new Exception("Test error"); + var result = new BulkProcessResult(handlingError, null); + + // Act + var catchBlockExceptions = result.ToCatchBlockExceptions().ToList(); + + // Assert + Assert.That(catchBlockExceptions, Is.Empty); + } + + [Test] + public void Should_PreserveHandlingErrorInCatchBlockExceptions() + { + // Arrange + var handlingError = new Exception("Original handling error"); + var processErrors = new List + { + new ErrorProcessorException(new Exception("Error 1"), null, ProcessStatus.Faulted) + }; + var result = new BulkProcessResult(handlingError, processErrors); + + // Act + var catchBlockExceptions = result.ToCatchBlockExceptions(); + + // Assert + Assert.That(catchBlockExceptions.Count, Is.EqualTo(1)); + Assert.That(catchBlockExceptions.First().InnerException, Is.EqualTo(handlingError)); + } + } +} diff --git a/tests/ConvertExceptionDelegatesTests.cs b/tests/ConvertExceptionDelegatesTests.cs new file mode 100644 index 00000000..d396d368 --- /dev/null +++ b/tests/ConvertExceptionDelegatesTests.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using System; + +namespace PoliNorError.Tests +{ + [TestFixture] + internal class ConvertExceptionDelegatesTests + { + // Custom exception for testing inheritance +#pragma warning disable RCS1194 // Implement exception constructors. + private class CustomTestException : InvalidOperationException { } +#pragma warning disable S3871 // Exception types should be "public" + private class UnrelatedException : Exception { } +#pragma warning restore S3871 // Exception types should be "public" +#pragma warning restore RCS1194 // Implement exception constructors. + + [Test] + public void Should_ReturnTrue_When_ExceptionIsExactType() + { + // Arrange + var original = new InvalidOperationException("Test message"); + + // Act + bool result = ConvertExceptionDelegates.ToSubException(original, out var typedException); + + // Assert + Assert.That(result, Is.True); + Assert.That(typedException, Is.SameAs(original)); + } + + [Test] + public void Should_ReturnTrue_When_ExceptionIsSubclass() + { + // Arrange + var subclassEx = new CustomTestException(); + + // Act - Checking if a CustomTestException can be treated as an InvalidOperationException + bool result = ConvertExceptionDelegates.ToSubException(subclassEx, out var typedException); + + // Assert + Assert.That(result, Is.True); + Assert.That(typedException, Is.Not.Null); + Assert.That(typedException, Is.InstanceOf()); + } + + [Test] + public void Should_ReturnFalse_When_ExceptionIsUnrelatedType() + { + // Arrange + var unrelated = new UnrelatedException(); + + // Act + bool result = ConvertExceptionDelegates.ToSubException(unrelated, out var typedException); + + // Assert + Assert.That(result, Is.False); + Assert.That(typedException, Is.Null); + } + + [Test] + public void Should_ReturnFalse_When_ExceptionIsNull() + { + // Arrange + Exception nullEx = null; + + // Act + bool result = ConvertExceptionDelegates.ToSubException(nullEx, out var typedException); + + // Assert + Assert.That(result, Is.False); + Assert.That(typedException, Is.Null); + } + } +} diff --git a/tests/DefaultFallbackProcessorTests.cs b/tests/DefaultFallbackProcessorTests.cs index fb3377da..c1c605ff 100644 --- a/tests/DefaultFallbackProcessorTests.cs +++ b/tests/DefaultFallbackProcessorTests.cs @@ -699,5 +699,159 @@ public void Should_FallbackT_BeCancelable() Assert.That(tryResCount.NoError, Is.True); } } + + [Test] + [TestCase(true, true, true)] + [TestCase(false, true, true)] + [TestCase(true, false, true)] + [TestCase(false, false, true)] + [TestCase(true, true, false)] + [TestCase(false, true, false)] + [TestCase(true, false, false)] + [TestCase(false, false, false)] + public void Should_Fallback_Action_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll, bool withContext) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new DefaultFallbackProcessor(); + + PolicyResult pr; + + if (waitAll) + { + if (withContext) + { + pr = processor.Fallback(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), 5, (_) => { }, cts.Token); + } + else + { + pr = processor.Fallback(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), (_) => { }, cts.Token); + } + } + else + { + if (withContext) + { + pr = processor.Fallback(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), 5, (_) => { }, cts.Token); + } + else + { + pr = processor.Fallback(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), (_) => { }, cts.Token); + } + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void Should_Fallback_ActionWithParam_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new DefaultFallbackProcessor(); + + PolicyResult pr; + + if (waitAll) + { + pr = processor.Fallback(TaskWaitingDelegates.GetActionWithParamWithTaskWaitAll(cts, canceledOnLinkedSource), 4, (_) => { }, cts.Token); + } + else + { + pr = processor.Fallback(TaskWaitingDelegates.GetActionWithParamWithTaskWait(cts, canceledOnLinkedSource), 4, (_) => { }, cts.Token); + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void Should_Fallback_Func_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new DefaultFallbackProcessor(); + + PolicyResult pr; + + if (waitAll) + { + pr = processor.Fallback(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), (_) => 1, cts.Token); + } + else + { + pr = processor.Fallback(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), (_) => 1, cts.Token); + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void Should_Fallback_Func_WithParam_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new DefaultFallbackProcessor(); + + PolicyResult pr; + + if (waitAll) + { + pr = processor.Fallback(TaskWaitingDelegates.GetFuncWithParamWithTaskWaitAll(cts, canceledOnLinkedSource), 1, (_) => 1, cts.Token); + } + else + { + pr = processor.Fallback(TaskWaitingDelegates.GetFuncWithParamWithTaskWait(cts, canceledOnLinkedSource), 1, (_) => 1, cts.Token); + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } } } \ No newline at end of file diff --git a/tests/ErrorProcessorTests.cs b/tests/ErrorProcessorTests.cs index 21abf861..7b1a7c5c 100644 --- a/tests/ErrorProcessorTests.cs +++ b/tests/ErrorProcessorTests.cs @@ -211,5 +211,39 @@ public async Task Should_DefaultErrorProcessor_TParam_Of_Action_With_TokenParam_ await errPr.ProcessAsync(new Exception(), piToTest); Assert.That(i, Is.EqualTo(isGeneric ? 1 : 0)); } + + [Test] + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public async Task Should_DefaultTypedErrorProcessor_Of_Action_With_TokenParam_Process_Only_Typed_Exception(bool errCanBeProcessed, bool isSync) + { + int i = 0; + var processor = new DefaultTypedErrorProcessor((ex, _, __) => { if (ex.ParamName == "Test") i++; }); + + Exception exToTest = null; + + if (errCanBeProcessed) + { +#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one + exToTest = new ArgumentException("", "Test"); +#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one + } + else + { + exToTest = new Exception(""); + } + + if (isSync) + { + processor.Process(exToTest, new ProcessingErrorInfo(PolicyAlias.NotSet)); + } + else + { + await processor.ProcessAsync(exToTest, new ProcessingErrorInfo(PolicyAlias.NotSet)); + } + Assert.That(i, Is.EqualTo(errCanBeProcessed ? 1 : 0)); + } } } diff --git a/tests/ErrorSetTests.cs b/tests/ErrorSetTests.cs index 12020f66..aa0cadc6 100644 --- a/tests/ErrorSetTests.cs +++ b/tests/ErrorSetTests.cs @@ -1,12 +1,21 @@ using NUnit.Framework; using System; using System.Linq; -using static PoliNorError.ErrorSet; namespace PoliNorError.Tests { public class ErrorSetTests { +#pragma warning disable RCS1194 // Implement exception constructors. +#pragma warning disable S3376 // Attribute, EventArgs, and Exception type names should end with the type being extended +#pragma warning disable S3871 // Exception types should be "public" + private class TestException1 : Exception { } + private class TestException2 : Exception { } + private class TestException3 : Exception { } +#pragma warning restore S3871 // Exception types should be "public" +#pragma warning restore S3376 // Attribute, EventArgs, and Exception type names should end with the type being extended +#pragma warning restore RCS1194 // Implement exception constructors. + [Test] [TestCase(false)] [TestCase(true)] @@ -33,8 +42,114 @@ public void Should_WithError_Add_Error_Type() [Test] public void Should_ErrorSetItem_Be_Equatable_With_Null() { - var esi = new ErrorSetItem(typeof(InvalidOperationException), ErrorSetItem.ItemType.Error); + var esi = new ErrorSet.ErrorSetItem(typeof(InvalidOperationException), ErrorSet.ErrorSetItem.ItemType.Error); Assert.That(esi.Equals(null), Is.False); } - } + + [Test] + public void Should_ContainTwoErrorItems_When_CallingFromErrorsWithTwoGenericTypes() + { + // Act + var result = ErrorSet.FromErrors(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Items, Has.Count.EqualTo(2)); + + var expectedItem1 = new ErrorSet.ErrorSetItem(typeof(TestException1), ErrorSet.ErrorSetItem.ItemType.Error); + var expectedItem2 = new ErrorSet.ErrorSetItem(typeof(TestException2), ErrorSet.ErrorSetItem.ItemType.Error); + + Assert.That(result.Items, Does.Contain(expectedItem1)); + Assert.That(result.Items, Does.Contain(expectedItem2)); + } + + [Test] + public void Should_ContainThreeErrorItems_When_CallingFromErrorsWithThreeGenericTypes() + { + // Act + var result = ErrorSet.FromErrors(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Items, Has.Count.EqualTo(3)); + + var expectedItem1 = new ErrorSet.ErrorSetItem(typeof(TestException1), ErrorSet.ErrorSetItem.ItemType.Error); + var expectedItem2 = new ErrorSet.ErrorSetItem(typeof(TestException2), ErrorSet.ErrorSetItem.ItemType.Error); + var expectedItem3 = new ErrorSet.ErrorSetItem(typeof(TestException3), ErrorSet.ErrorSetItem.ItemType.Error); + + Assert.That(result.Items, Does.Contain(expectedItem1)); + Assert.That(result.Items, Does.Contain(expectedItem2)); + Assert.That(result.Items, Does.Contain(expectedItem3)); + } + + [Test] + public void Should_MarkAllItemsAsErrorKind_When_CallingFromErrors() + { + // Act + var result = ErrorSet.FromErrors(); + + // Assert + foreach (var item in result.Items) + { + Assert.That(item.ErrorKind, Is.EqualTo(ErrorSet.ErrorSetItem.ItemType.Error), + "FromErrors should only add items with ItemType.Error"); + } + } + + [Test] + public void Should_NotAddDuplicateItems_When_CallingFromErrorsWithSameTypeMultipleTimes() + { + // Act + var result = ErrorSet.FromErrors(); + + // Assert + // HashSet implementation should prevent duplicates based on ErrorType and ErrorKind + Assert.That(result.Items, Has.Count.EqualTo(1)); + + var expectedItem = new ErrorSet.ErrorSetItem(typeof(TestException1), ErrorSet.ErrorSetItem.ItemType.Error); + Assert.That(result.Items, Does.Contain(expectedItem)); + } + + [Test] + public void Should_HasErrorGeneric_ReturnTrue_When_ErrorTypeExists() + { + var errorSet = ErrorSet.FromError(); + + var result = errorSet.HasError(); + + Assert.That(result, Is.True); + } + + [Test] + public void Should_HasErrorGeneric_ReturnFalse_When_ErrorTypeMissing() + { + var errorSet = ErrorSet.FromError(); + + var result = errorSet.HasError(); + + Assert.That(result, Is.False); + } + + [Test] + public void Should_HasInnerErrorGeneric_ReturnTrue_When_InnerErrorTypeExists() + { + var errorSet = ErrorSet.FromError() + .WithInnerError(); + + var result = errorSet.HasInnerError(); + + Assert.That(result, Is.True); + } + + [Test] + public void Should_HasInnerErrorGeneric_ReturnFalse_When_InnerErrorTypeMissing() + { + var errorSet = ErrorSet.FromError() + .WithInnerError(); + + var result = errorSet.HasInnerError(); + + Assert.That(result, Is.False); + } + } } diff --git a/tests/ErrorWithInnerExcThrowingFuncs.cs b/tests/ErrorWithInnerExcThrowingFuncs.cs index b06cbb94..15461e14 100644 --- a/tests/ErrorWithInnerExcThrowingFuncs.cs +++ b/tests/ErrorWithInnerExcThrowingFuncs.cs @@ -7,6 +7,9 @@ namespace PoliNorError.Tests public static class ErrorWithInnerExcThrowingFuncs { public static void ActionWithInner() => throw new TestExceptionWithInnerException(); + + public static void ActionWithParamWithInner(int _) => throw new TestExceptionWithInnerException(); + public static void ActionWithInnerWithMsg(string innerExceptionMsg) => throw new TestExceptionWithInnerException("", innerExceptionMsg); public static void Action() => throw new Exception(); @@ -20,8 +23,12 @@ public static class ErrorWithInnerExcThrowingFuncs public static async Task AsyncFuncWithInnerT(CancellationToken _) { await Task.Delay(1); throw new TestExceptionWithInnerException(""); } + public static async Task AsyncFuncWithParamWithInnerT(int _, CancellationToken token) { await Task.Delay(1, token); throw new TestExceptionWithInnerException(""); } + public static int FuncWithInner() => throw new TestExceptionWithInnerException(); + public static int FuncWithParamWithInner(int _) => throw new TestExceptionWithInnerException(); + public class TestExceptionWithInnerException : Exception { public TestExceptionWithInnerException() : this("", new TestInnerException()) diff --git a/tests/ExceptionExtensionsTests.cs b/tests/ExceptionExtensionsTests.cs index 8354f53c..471cadd3 100644 --- a/tests/ExceptionExtensionsTests.cs +++ b/tests/ExceptionExtensionsTests.cs @@ -20,7 +20,7 @@ public void Should_Return_New_OperationCanceledException_When_Not_Found() // Assert Assert.That(result, Is.Not.Null); - Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.TypeOf()); } [Test] diff --git a/tests/ExceptionFilterTests.cs b/tests/ExceptionFilterTests.cs index ed4d3e0f..f620f2db 100644 --- a/tests/ExceptionFilterTests.cs +++ b/tests/ExceptionFilterTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using static PoliNorError.PolicyProcessor; namespace PoliNorError.Tests { @@ -253,6 +254,71 @@ public void Should_FilledCatchBlockFilter_CreateByExcluding_Add_ErrorFilter_Expr Assert.That(filter.ErrorFilter.IncludedErrorFilters.Count(), Is.EqualTo(1)); } + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Add_ErrorFilter_FromFunc_Or_ByExpression(bool fromFunc) + { + var filter = new ExceptionFilter(); + if (fromFunc) + { + filter.IncludeError((_) => true); + filter.ExcludeError((_) => true); + } + else + { + filter.IncludeError((_) => true); + filter.ExcludeError((_) => true); + } + + Assert.That(filter.IncludedErrorFilters.Count(), Is.EqualTo(1)); + Assert.That(filter.ExcludedErrorFilters.Count(), Is.EqualTo(1)); + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Should_IncludeError_ForInnerError(bool errFilterUnsatisfied) + { + var filter = new ExceptionFilter(); + filter.IncludeError().IncludeError(CatchBlockFilter.ErrorType.InnerError); + + Exception errorToHandler; + if (errFilterUnsatisfied) + { + errorToHandler = new TestExceptionWithInnerArgumentNullException(); + } + else + { + errorToHandler = new TestExceptionWithInnerArgumentException(); + } + + var actualErrFilterUnsatisfied = !filter.GetCanHandle()(errorToHandler); + Assert.That(actualErrFilterUnsatisfied, Is.EqualTo(errFilterUnsatisfied)); + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Should_ExcludeError_ForInnerError(bool errFilterUnsatisfied) + { + var filter = new ExceptionFilter(); + filter.ExcludeError().ExcludeError(CatchBlockFilter.ErrorType.InnerError); + + Exception errorToHandler; + if (errFilterUnsatisfied) + { + errorToHandler = new TestExceptionWithInnerArgumentException(); + } + else + { + errorToHandler = new TestExceptionWithInnerArgumentNullException(); + } + + var actualErrFilterUnsatisfied = !filter.GetCanHandle()(errorToHandler); + Assert.That(actualErrFilterUnsatisfied, Is.EqualTo(errFilterUnsatisfied)); + } + [Test] [TestCase(false)] [TestCase(true)] @@ -450,6 +516,32 @@ public void Should_NonEmptyCatchBlockFilter_CreateByExcluding_Works_Correctly_Fo var actualErrFilterUnsatisfied = !filter.ErrorFilter.GetCanHandle()(errorToHandler); Assert.That(actualErrFilterUnsatisfied, Is.EqualTo(errFilterUnsatisfied)); } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_ExceptionFilter_IncludeErrorSet(bool inner) + { + var errorSet = ErrorSet.FromError().WithInnerError(); + var filter = new PolicyProcessor.ExceptionFilter(); + filter = filter.IncludeErrorSet(errorSet); + var canHandle = IsErrorCanBeHandledByNonEmptyCatchBlockFilter(new NonEmptyCatchBlockFilter() { ErrorFilter = filter }, inner); + Assert.That(canHandle, Is.True); + Assert.That(filter, Is.Not.Null); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_ExceptionFilter_ExcludeErrorSet(bool inner) + { + var errorSet = ErrorSet.FromError().WithInnerError(); + var filter = new PolicyProcessor.ExceptionFilter(); + filter = filter.ExcludeErrorSet(errorSet); + var canHandle = IsErrorCanBeHandledByNonEmptyCatchBlockFilter(new NonEmptyCatchBlockFilter() { ErrorFilter = filter }, inner); + Assert.That(canHandle, Is.False); + Assert.That(filter, Is.Not.Null); + } } [TestFixture] diff --git a/tests/ICanAddErrorFilterTests.cs b/tests/ICanAddErrorFilterTests.cs index 96bff994..9928aec2 100644 --- a/tests/ICanAddErrorFilterTests.cs +++ b/tests/ICanAddErrorFilterTests.cs @@ -520,5 +520,17 @@ public void Should_DefaultFallbackProcessor_FilterErrors_WhenErrorFilterIsAdded_ Assert.That(fallbackProcessor.Fallback(() => throw errorToHandle, (_) => { }).ErrorFilterUnsatisfied, Is.EqualTo(excludeFilterWork)); } + + [Test] + public void Should_AddAddExceptionFilter() + { + var retryPolicy = new RetryPolicy(1); + var filter = new PolicyProcessor.ExceptionFilter(); + filter.AddExcludedErrorFilter((_) => true); + retryPolicy.AddExceptionFilter(filter); + var result = retryPolicy.Handle(() => throw new InvalidOperationException()); + Assert.That(result.ErrorFilterUnsatisfied, Is.True); + Assert.That(result.IsFailed, Is.True); + } } } diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index bfff3372..46e2dcb8 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -1,11 +1,11 @@  - - - - - - + + + + + + Debug @@ -48,11 +48,11 @@ ..\packages\NSubstitute.5.3.0\lib\net462\NSubstitute.dll True - - ..\packages\NUnit.4.4.0\lib\net462\nunit.framework.dll + + ..\packages\NUnit.4.5.1\lib\net462\nunit.framework.dll - - ..\packages\NUnit.4.4.0\lib\net462\nunit.framework.legacy.dll + + ..\packages\NUnit.4.5.1\lib\net462\nunit.framework.legacy.dll @@ -90,8 +90,10 @@ + + @@ -115,6 +117,8 @@ + + @@ -124,6 +128,8 @@ + + @@ -176,12 +182,12 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - + + + + + + - + \ No newline at end of file diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs new file mode 100644 index 00000000..77eeee1c --- /dev/null +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -0,0 +1,487 @@ +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static PoliNorError.BulkErrorProcessor; + +namespace PoliNorError.Tests +{ + [TestFixture] + public partial class PolicyProcessorTests + { + private class TestPolicyProcessor : PolicyProcessor + { + public TestPolicyProcessor(IBulkErrorProcessor bulkErrorProcessor, bool testFilterUnsatisfied = false) + { + _bulkErrorProcessor = bulkErrorProcessor; + if (testFilterUnsatisfied) + { + ErrorFilter.AddExcludedErrorFilter(); + } + } + + public ExceptionHandlingResult TestHandleException( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + CancellationToken token, + ProcessingOrder processingOrder = ProcessingOrder.EvaluateThenProcess, + Func, CancellationToken, bool> policyRuleFunc = null, + ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate, + Action, CancellationToken> errorSaver = null) + { + return HandleException(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, processingOrder, cancellationEffect, token); + } + + public Task TestHandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Func, bool, CancellationToken, Task> errorSaver, + Func, CancellationToken, Task> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + ErrorProcessingCancellationEffect cancellationEffect, + bool configureAwait, + ProcessingOrder processingOrder = ProcessingOrder.EvaluateThenProcess, + CancellationToken token = default) + { + return HandleExceptionAsync(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, processingOrder, cancellationEffect, configureAwait, token); + } + } + + private class TestErrorContext : ErrorContext + { + public TestErrorContext(string context) : base(context) { } + + public override ProcessingErrorContext ToProcessingErrorContext() + { + return new ProcessingErrorContext(); + } + } + + private class TestBulkErrorProcessor : IBulkErrorProcessor + { + public BulkProcessResult ResultToReturn { get; set; } + + public bool IsCanceled { get; set; } + + public bool IsProcessed { get; private set; } + + public void AddProcessor(IErrorProcessor errorProcessor) => throw new NotImplementedException(); + public IEnumerator GetEnumerator() => throw new NotImplementedException(); + + public BulkProcessResult Process(Exception handlingError, ProcessingErrorContext errorContext = null, CancellationToken token = default) + { + IsProcessed = true; + return ResultToReturn ?? new BulkProcessResult(handlingError, null); + } + + public Task ProcessAsync(Exception handlingError, ProcessingErrorContext errorContext = null, bool configAwait = false, CancellationToken token = default) + { + var result = new BulkProcessResult(handlingError, ProcessErrors ?? Array.Empty(), IsCanceled); + return Task.FromResult(result); + } + + IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); + public ErrorProcessorException[] ProcessErrors { get; set; } + } + + [Test] + public void Should_ReturnHandled_AndSetFailedAndCanceled_WhenTokenIsCanceled() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new Exception("test exception"); + using (var cts = new CancellationTokenSource()) + { + cts.Cancel(); + + // Act + var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.IsCanceled, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + } + + [Test] + [TestCase(ErrorProcessingCancellationEffect.Ignore)] + [TestCase(ErrorProcessingCancellationEffect.Propagate)] + public void Should_Set_PolicyResult_IsCanceled_DependOn_CancellationEffect(ErrorProcessingCancellationEffect cancellationEffect) + { + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new Exception("test exception"); + using (var cts = new CancellationTokenSource()) + { + var bulkProcessor = new BulkErrorProcessor() + .WithErrorProcessorOf((Exception _, CancellationToken __) => cts.Cancel()); + + var processor = new TestPolicyProcessor(bulkProcessor); + + var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token, ProcessingOrder.EvaluateThenProcess, null, ExceptionHandlingBehavior.Handle, cancellationEffect); + + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + + Assert.That(policyResult.IsFailed, Is.EqualTo(cancellationEffect == ErrorProcessingCancellationEffect.Propagate)); + Assert.That(policyResult.IsCanceled, Is.EqualTo(cancellationEffect == ErrorProcessingCancellationEffect.Propagate)); + + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + } + + [Test] + public void Should_Not_Set_PolicyResult_IsCanceled_When_ProcessingOrder_ProcessThenEvaluate_And_CancellationEffectIgnore() + { + // Arrange + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new Exception("test exception"); + using (var cts = new CancellationTokenSource()) + { + var bulkProcessor = new BulkErrorProcessor() + .WithErrorProcessorOf((Exception _, CancellationToken __) => cts.Cancel()); + + var processor = new TestPolicyProcessor(bulkProcessor); + + // Act + var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token, ProcessingOrder.ProcessThenEvaluate, cancellationEffect: ErrorProcessingCancellationEffect.Ignore); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Accepted)); + Assert.That(policyResult.IsFailed, Is.False); + Assert.That(policyResult.IsCanceled, Is.False); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + } + + [Test] + public void Should_ReturnHandled_WhenNullPolicyRuleFunc() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = new BulkProcessResult(new InvalidOperationException(), null) + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + // Act + var result = processor.TestHandleException(exception, policyResult, errorContext, CancellationToken.None); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_ReturnRethrow_WhenErrorFilterFailsAndBehaviorIsConditionalRethrow() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor, true); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new ArgumentException("test exception"); + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + null, + ExceptionHandlingBehavior.ConditionalRethrow); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Rethrow)); + } + + [Test] + public void Should_ReturnHandled_WhenErrorFilterFailsAndBehaviorIsHandle() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = new BulkProcessResult(new ArgumentException("test"), null) + }; + var processor = new TestPolicyProcessor(bulkProcessor, true); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new ArgumentException("test"); + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + null, + ExceptionHandlingBehavior.Handle); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_ReturnHandled_AndSetFailed_WhenPolicyRuleFuncReturnsFalse() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + bool policyRuleFunc(ErrorContext _, CancellationToken __) => false; + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + policyRuleFunc); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_ReturnHandled_WhenPolicyRuleFuncReturnsTrue() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = new BulkProcessResult(new InvalidOperationException(), null) + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + bool policyRuleFunc(ErrorContext _, CancellationToken __) => true; + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + policyRuleFunc); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_SetFailedAndCanceled_WhenBulkProcessResultIsCanceled() + { + // Arrange + var bulkProcessResult = new BulkProcessResult(new InvalidOperationException(), null, true); + + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = bulkProcessResult + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.IsCanceled, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_AddBulkProcessorErrors_WhenBulkProcessorReturnsErrors() + { + // Arrange + var processorException = new ErrorProcessorException(new InvalidCastException(), null, ProcessStatus.Faulted); + var bulkProcessResult = new BulkProcessResult( + new InvalidOperationException(), + new[] { processorException }); + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = bulkProcessResult + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + // Act + processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None); + + // Assert + Assert.That(policyResult.IsFailed, Is.False); // No critical errors + Assert.That(bulkProcessor.IsProcessed, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_AddExceptionToPolicyResult_WhenHandlingException() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = new BulkProcessResult(new InvalidOperationException(), null) + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + // Act + processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None); + + // Assert - exception is added internally + Assert.That(policyResult.IsFailed, Is.False); + Assert.That(bulkProcessor.IsProcessed, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + public void Should_UseCustomErrorSaver_WhenProvided() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = new BulkProcessResult(new InvalidOperationException(), null) + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + var customErrorSaverCalled = false; + void customErrorSaver(PolicyResult pr, Exception _, ErrorContext __, CancellationToken ___) + { + customErrorSaverCalled = true; + pr.AddError(new Exception("Custom error")); + } + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Propagate, + errorSaver:customErrorSaver); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(customErrorSaverCalled, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [Test] + [TestCase(ExceptionHandlingBehavior.Handle)] + [TestCase(ExceptionHandlingBehavior.ConditionalRethrow)] + public void Should_HandleErrorFilterThrowingException_WhenHandleOrRethrowBehavior(ExceptionHandlingBehavior handlingBehavior) + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor); + + // Make the error filter throw an exception + processor.ErrorFilter.AddIncludedErrorFilter((ex) => Save(ex)); + + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + null, + handlingBehavior + ); + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.CriticalError, Is.Not.Null); + Assert.That(policyResult.CatchBlockErrors.Count, Is.EqualTo(1)); + } + +#pragma warning disable S1172 // Unused method parameters should be removed + private bool Save(Exception _) => throw new InvalidOperationException("Filter error"); +#pragma warning restore S1172 // Unused method parameters should be removed + + [Test] + public void Should_ReturnHandled_WhenConditionalRethrowWithValidFilterAndPolicyRule() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor + { + ResultToReturn = new BulkProcessResult(new InvalidOperationException(), null) + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new InvalidOperationException("test exception"); + + bool policyRuleFunc(ErrorContext _, CancellationToken __) => true; + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, + policyRuleFunc, + ExceptionHandlingBehavior.ConditionalRethrow); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + } +} diff --git a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs new file mode 100644 index 00000000..57ea1d29 --- /dev/null +++ b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs @@ -0,0 +1,505 @@ +using NUnit.Framework; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using static PoliNorError.BulkErrorProcessor; + +namespace PoliNorError.Tests +{ + [TestFixture] + public partial class PolicyProcessorTests + { + [Test] + public async Task Should_ReturnHandled_WhenHandlingBehaviorIsHandle() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + } + + [Test] + public async Task Should_AddErrorToPolicyResult_WhenErrorSaverIsNull() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(policyResult.NoError, Is.False); + } + + [Test] + public async Task Should_CallCustomErrorSaver_WhenProvided() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + var errorSaverCalled = false; + + Task errorSaver(PolicyResult _, Exception __, ErrorContext ___, bool ____, CancellationToken _____) + { + errorSaverCalled = true; + return Task.CompletedTask; + } + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + errorSaver, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(errorSaverCalled, Is.True); + } + + [Test] + public async Task Should_SetFailedAndCanceled_WhenTokenIsCanceledAfterErrorSaver() + { + var processor = new TestPolicyProcessor(null); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + using (var cts = new CancellationTokenSource()) + { + Task errorSaver(PolicyResult _, Exception __, ErrorContext ___, bool ____, CancellationToken _____) + { + cts.Cancel(); + return Task.CompletedTask; + } + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + errorSaver, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false, + ProcessingOrder.EvaluateThenProcess, + cts.Token); + } + + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.IsCanceled, Is.True); + } + + [Test] + public async Task Should_ReturnRethrow_WhenPolicyRuleFuncReturnsFalseAndBehaviorIsConditionalRethrow() + { + var processor = new TestPolicyProcessor(null); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + Task policyRuleFunc(ErrorContext _, CancellationToken __) => Task.FromResult(false); + + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + policyRuleFunc, + ExceptionHandlingBehavior.ConditionalRethrow, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + } + + [Test] + public async Task Should_CallBulkErrorProcessor_WhenExceptionIsAccepted() + { + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(policyResult.IsSuccess, Is.True); + } + + [Test] + public async Task Should_AddBulkProcessorErrorsToPolicyResult() + { + var bulkProcessor = new TestBulkErrorProcessor + { + ProcessErrors = new[] { new ErrorProcessorException(new InvalidCastException(), null, ProcessStatus.Faulted) } + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(policyResult.IsSuccess, Is.True); + } + + [Test] + public async Task Should_SetFailedAndCanceled_WhenBulkProcessorIsCanceledAndEffectIsPropagate() + { + var bulkProcessor = new TestBulkErrorProcessor + { + IsCanceled = true + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Propagate, + false); + + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.IsCanceled, Is.True); + } + + [Test] + public async Task Should_NotSetCanceled_WhenBulkProcessorIsCanceledAndEffectIsIgnore() + { + var bulkProcessor = new TestBulkErrorProcessor + { + IsCanceled = true + }; + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(policyResult.IsCanceled, Is.False); + } + + [Test] + public async Task Should_CallErrorSaverBeforeDeterminingResult_WhenBehaviorIsNotConditionalRethrow() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + var callOrder = 0; + var errorSaverOrder = 0; + + Task errorSaver(PolicyResult _, Exception __, ErrorContext ___, bool ____, CancellationToken _____) + { + errorSaverOrder = ++callOrder; + return Task.CompletedTask; + } + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + errorSaver, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(errorSaverOrder, Is.EqualTo(1)); + } + + [Test] + public async Task Should_CallErrorSaverAfterDeterminingResult_WhenBehaviorIsConditionalRethrowAndExceptionAccepted() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + var errorSaverCallCount = 0; + + Task errorSaver(PolicyResult _, Exception __, ErrorContext ___, bool ____, CancellationToken _____) + { + errorSaverCallCount++; + return Task.CompletedTask; + } + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + errorSaver, + null, + ExceptionHandlingBehavior.ConditionalRethrow, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(errorSaverCallCount, Is.EqualTo(1)); + } + + [Test] + public async Task Should_UseConfigureAwaitParameter_WhenCallingAsyncMethods() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + true); + + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + } + + [Test] + public async Task Should_PassCancellationTokenToBulkProcessor() + { + using (var cts = new CancellationTokenSource()) + { + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false, + ProcessingOrder.EvaluateThenProcess, + cts.Token); + + Assert.That(policyResult.IsSuccess, Is.True); + } + } + + [Test] + public async Task Should_SetFailedAndCanceled_WhenTokenCanceledAfterErrorSaverInConditionalRethrow() + { + var processor = new TestPolicyProcessor(null); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + using (var cts = new CancellationTokenSource()) + { + Task errorSaver(PolicyResult _, Exception __, ErrorContext ___, bool ____, CancellationToken _____) + { + cts.Cancel(); + return Task.CompletedTask; + } + + await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + errorSaver, + null, + ExceptionHandlingBehavior.ConditionalRethrow, + ErrorProcessingCancellationEffect.Ignore, + false, + ProcessingOrder.EvaluateThenProcess, + cts.Token); + } + + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.IsCanceled, Is.True); + } + + [Test] + public async Task Should_HandleExceptionAsync_ReturnHandled_WhenPolicyRuleFuncReturnsTrue() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + + Task policyRuleFunc(ErrorContext _, CancellationToken __) => Task.FromResult(true); + + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + policyRuleFunc, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + } + + [Test] + public async Task Should_ReturnRethrow_WhenExceptionFilterFailsAndBehaviorIsConditionalRethrow() + { + var processor = new TestPolicyProcessor(null, true); + var policyResult = new PolicyResult(true); + var exception = new ArgumentException("Test exception"); + var errorContext = new TestErrorContext("test"); + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.ConditionalRethrow, + ErrorProcessingCancellationEffect.Ignore, + false); + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Rethrow)); + } + + [Test] + public async Task Should_ReturnHandled_WhenExceptionFilterFailsAndBehaviorIsHandle() + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor(), true); + var policyResult = new PolicyResult(true); + var exception = new ArgumentException("Test exception"); + var errorContext = new TestErrorContext("test"); + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false); + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + } + + [Test] + [TestCase(ExceptionHandlingBehavior.Handle)] + [TestCase(ExceptionHandlingBehavior.ConditionalRethrow)] + public async Task Should_ReturnHandled_WhenExceptionFilterThrowsException(ExceptionHandlingBehavior handlingBehavior) + { + var processor = new TestPolicyProcessor(new TestBulkErrorProcessor()); + processor.ErrorFilter.AddIncludedErrorFilter((ex) => Save(ex)); + var policyResult = new PolicyResult(true); + var exception = new Exception("Test exception"); + var errorContext = new TestErrorContext("test"); + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + null, + handlingBehavior, + ErrorProcessingCancellationEffect.Ignore, + false); + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.CriticalError, Is.Not.Null); + Assert.That(policyResult.CatchBlockErrors.Count, Is.EqualTo(1)); + } + + [Test] + public async Task Should_HandleExceptionAsync_Not_Set_PolicyResult_IsCanceled_When_ProcessingOrder_ProcessThenEvaluate_And_CancellationEffectIgnore() + { + // Arrange + var policyResult = PolicyResult.ForNotSync(); + var errorContext = new TestErrorContext("test"); + var exception = new Exception("test exception"); + + bool ruleResult = false; + Task ruleFunc(ErrorContext _, CancellationToken __) + { + ruleResult = true; + return Task.FromResult(true); + } + + using (var cts = new CancellationTokenSource()) + { + var bulkProcessor = new BulkErrorProcessor() + .WithErrorProcessorOf((Exception _, CancellationToken __) => cts.Cancel()); + + var processor = new TestPolicyProcessor(bulkProcessor); + + // Act + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + ruleFunc, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false, + ProcessingOrder.ProcessThenEvaluate, + cts.Token); + + // Assert + Assert.That(ruleResult, Is.True); + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Accepted)); + Assert.That(policyResult.IsFailed, Is.False); + Assert.That(policyResult.IsCanceled, Is.False); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + } + } +} diff --git a/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index 2ca6c0d9..883a80c7 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -290,5 +290,323 @@ public void Should_Clamp_Delay_To_MaxDelay() Assert.That(retryDelay.GetDelay(2), Is.EqualTo(maxTime)); Assert.That(retryDelay.GetDelay(3), Is.EqualTo(maxTime)); } + + [Test] + public void Should_Implicitly_Convert_ConstantRetryDelayOptions_To_RetryDelay() + { + var crdo = new ConstantRetryDelayOptions() { BaseDelay = TimeSpan.FromMilliseconds(1) }; + var tester = new RetryDelayTester(); + Assert.That(tester.GetAttemptDelay(crdo), Is.EqualTo(TimeSpan.FromMilliseconds(1))); + } + + [Test] + public void Should_Implicitly_Convert_LinearRetryDelayOptions_To_RetryDelay() + { + var crdo = new LinearRetryDelayOptions() { BaseDelay = TimeSpan.FromMilliseconds(2), SlopeFactor = 2 }; + var tester = new RetryDelayTester(); + Assert.That(tester.GetAttemptDelay(crdo), Is.EqualTo(TimeSpan.FromMilliseconds(2 * crdo.SlopeFactor))); + } + + [Test] + public void Should_Implicitly_Convert_ExponentialRetryDelayOptions_To_RetryDelay() + { + var crdo = new ExponentialRetryDelayOptions() { BaseDelay = TimeSpan.FromMilliseconds(2), ExponentialFactor = 2 }; + var tester = new RetryDelayTester(); + Assert.That(tester.GetAttemptDelay(crdo), Is.EqualTo(TimeSpan.FromMilliseconds(2 * Math.Pow(crdo.ExponentialFactor, 0)))); + } + + [Test] + public void Should_Implicitly_Convert_TimeSeriesRetryDelayOptions_To_RetryDelay() + { + var crdo = new TimeSeriesRetryDelayOptions() { BaseDelay = TimeSpan.FromMilliseconds(2), Times = new TimeSpan[] {TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(2) } }; + var tester = new RetryDelayTester(); + Assert.That(tester.GetAttemptDelay(crdo), Is.EqualTo(TimeSpan.FromMilliseconds(1))); + } + + [Test] + public void Should_Apply_Jitter_To_ConstantRetryDelay_Within_Expected_Range_When_UseJitter_True() + { + var baseDelay = TimeSpan.FromMilliseconds(1000); + var options = new ConstantRetryDelayOptions + { + BaseDelay = baseDelay, + MaxDelay = TimeSpan.MaxValue, + UseJitter = true + }; + var delay = new ConstantRetryDelay(options); + + var result = delay.GetDelay(1); + var baseMs = baseDelay.TotalMilliseconds; + var offset = baseMs * RetryDelayConstants.JitterFactor / 2; + var minExpected = TimeSpan.FromMilliseconds(baseMs - offset); + var maxExpected = TimeSpan.FromMilliseconds(baseMs + offset); + + Assert.That(result, Is.GreaterThanOrEqualTo(minExpected)); + Assert.That(result, Is.LessThanOrEqualTo(maxExpected)); + } + + [Test] + public void Should_Cap_ConstantRetryDelay_At_MaxDelay_When_Jitter_Exceeds_Max() + { + var baseDelay = TimeSpan.FromMilliseconds(1000); + var maxDelay = TimeSpan.FromMilliseconds(1100); + var options = new ConstantRetryDelayOptions + { + BaseDelay = baseDelay, + MaxDelay = maxDelay, + UseJitter = true + }; + var delay = new ConstantRetryDelay(options); + + // Force jitter to exceed max by using a known random value + // Note: This test assumes internal implementation details + var result = delay.GetDelay(1); + + Assert.That(result, Is.LessThanOrEqualTo(maxDelay)); + } + + [Test] + public void Should_Create_ConstantRetryDelay_Instance_Via_Create_Method() + { + var baseDelay = TimeSpan.FromMilliseconds(200); + var instance = ConstantRetryDelay.Create(baseDelay, useJitter: true); + + Assert.That(instance, Is.Not.Null); + Assert.That(instance.GetDelay(1), Is.GreaterThanOrEqualTo(TimeSpan.FromMilliseconds(150))); + } + + [Test] + public void Should_Apply_Jitter_To_LinearRetryDelay_Within_Expected_Range_When_UseJitter_True() + { + var baseDelay = TimeSpan.FromMilliseconds(100); + var options = new LinearRetryDelayOptions + { + BaseDelay = baseDelay, + UseJitter = true, + SlopeFactor = 1.0 + }; + var delay = new LinearRetryDelay(options); + + var result = delay.GetDelay(1); + var baseValue = 200.0; // 2 * 100 * 1.0 + var jitterRange = baseValue * RetryDelayConstants.JitterFactor / 2; + var minExpected = TimeSpan.FromMilliseconds(baseValue - jitterRange); + var maxExpected = TimeSpan.FromMilliseconds(baseValue + jitterRange); + + Assert.That(result, Is.GreaterThanOrEqualTo(minExpected)); + Assert.That(result, Is.LessThanOrEqualTo(maxExpected)); + } + + [Test] + public void Should_Cap_LinearRetryDelay_At_MaxDelay_When_Exceeded() + { + var baseDelay = TimeSpan.FromMilliseconds(100); + var maxDelay = TimeSpan.FromMilliseconds(250); + var options = new LinearRetryDelayOptions + { + BaseDelay = baseDelay, + MaxDelay = maxDelay, + UseJitter = false, + SlopeFactor = 1.0 + }; + var delay = new LinearRetryDelay(options); + + var result = delay.GetDelay(2); // Should be 300ms without cap + + Assert.That(result, Is.EqualTo(maxDelay)); + } + + [Test] + public void Should_Create_LinearRetryDelay_Instance_With_Create_Method_Default_Slope() + { + var baseDelay = TimeSpan.FromMilliseconds(100); + var instance = LinearRetryDelay.Create(baseDelay); + + var result = instance.GetDelay(1); + + Assert.That(result, Is.EqualTo(TimeSpan.FromMilliseconds(200))); + } + + [Test] + public void Should_ApplyMaxDelayInCreateMethods_ToTimeSeriesRetryDelay_WhenMaxDelaySpecified() + { + // Arrange + TimeSpan? maxDelay = TimeSpan.FromSeconds(2); + + // Act + var delay = TimeSeriesRetryDelay.Create(TimeSpan.FromSeconds(5), maxDelay); + var result = delay.GetDelay(0); + + // Assert + Assert.That(result, Is.EqualTo(maxDelay)); + } + + [Test] + public void Should_UseJitteredValues_ForTimeSeriesRetryDelay_WhenJitterEnabled() + { + // Arrange + var times = new[] { TimeSpan.FromSeconds(1) }; + var options = new TimeSeriesRetryDelayOptions + { + BaseDelay = TimeSpan.FromSeconds(1), + MaxDelay = TimeSpan.FromSeconds(10), + UseJitter = true, + Times = times + }; + var delay = new TimeSeriesRetryDelay(options); + + // Act + var results = Enumerable.Range(0, 10).Select(_ => delay.GetDelay(0)).ToArray(); + + // Assert - With jitter, we should get some variation in results + Assert.That(results.Distinct().Count(), Is.GreaterThan(1)); + Assert.That(results.ToList().TrueForAll(r => r.TotalMilliseconds >= 500 && r.TotalMilliseconds <= 1500), Is.True); + } + + [Test] + public void Should_UseNonJitteredValues_ForTimeSeriesRetryDelay_WhenJitterDisabled() + { + // Arrange + var times = new[] { TimeSpan.FromSeconds(1) }; + var options = new TimeSeriesRetryDelayOptions + { + BaseDelay = TimeSpan.FromSeconds(1), + MaxDelay = TimeSpan.FromSeconds(10), + UseJitter = false, + Times = times + }; + var delay = new TimeSeriesRetryDelay(options); + + // Act + var results = Enumerable.Range(0, 10).Select(_ => delay.GetDelay(0)).ToArray(); + + // Assert - Without jitter, all results should be identical + Assert.That(results.Distinct().Count(), Is.EqualTo(1)); + Assert.That(results[0], Is.EqualTo(TimeSpan.FromSeconds(1))); + } + + [TestFixture] + public class DecorrelatedJitterTests + { + private ExponentialRetryDelay.DecorrelatedJitter _jitter; + + [SetUp] + public void Setup() + { + _jitter = new ExponentialRetryDelay.DecorrelatedJitter( + TimeSpan.FromMilliseconds(100), + 2.0, + TimeSpan.FromMinutes(5)); + } + + [Test] + public void Should_GeneratePositiveDelaysForAllAttempts() + { + // Act & Assert + for (int i = 0; i < 10; i++) + { + var delay = _jitter.DecorrelatedJitterBackoffV2(i); + Assert.That(delay.TotalMilliseconds, Is.GreaterThan(0)); + } + } + + [Test] + public void Should_RespectMaxDelayLimit() + { + // Arrange + var maxDelay = TimeSpan.FromMilliseconds(500); + var jitter = new ExponentialRetryDelay.DecorrelatedJitter( + TimeSpan.FromMilliseconds(100), + 2.0, + maxDelay); + + // Act + var delay = jitter.DecorrelatedJitterBackoffV2(100); // Very high attempt + + // Assert + Assert.That(delay, Is.LessThanOrEqualTo(maxDelay)); + } + + [Test] + public void Should_HandleInfinityCase() + { + // Arrange + var maxDelay = TimeSpan.FromSeconds(30); + var jitter = new ExponentialRetryDelay.DecorrelatedJitter( + TimeSpan.FromMilliseconds(100), + 2.0, + maxDelay); + + // Act + var delay = jitter.DecorrelatedJitterBackoffV2(1024); // This should trigger infinity case + + // Assert + Assert.That(delay, Is.EqualTo(maxDelay)); + } + + [Test] + public void Should_ProduceVariedDelaysWithSameAttempt() + { + // Arrange + var delays = new TimeSpan[10]; + + // Act + for (int i = 0; i < 10; i++) + { + delays[i] = _jitter.DecorrelatedJitterBackoffV2(1); + } + + // Assert - Due to randomization, not all delays should be identical + bool hasVariation = false; + for (int i = 1; i < delays.Length; i++) + { + if (delays[i] != delays[0]) + { + hasVariation = true; + break; + } + } + Assert.That(hasVariation, Is.True); + } + + [Test] + public void Should_GenerateReasonableDelayProgression() + { + // Arrange + var baseDelay = TimeSpan.FromMilliseconds(100); + var jitter = new ExponentialRetryDelay.DecorrelatedJitter( + baseDelay, + 2.0, + TimeSpan.FromMinutes(5)); + + // Act + var delay0 = jitter.DecorrelatedJitterBackoffV2(0); + var delay1 = jitter.DecorrelatedJitterBackoffV2(1); + var delay2 = jitter.DecorrelatedJitterBackoffV2(2); + + // Assert - Generally, delays should increase (though jitter may cause some variation) + Assert.That(delay0.TotalMilliseconds, Is.GreaterThan(0)); + Assert.That(delay1.TotalMilliseconds, Is.GreaterThan(0)); + Assert.That(delay2.TotalMilliseconds, Is.GreaterThan(0)); + } + + [Test] + public void Should_HandleZeroAttempt() + { + // Act + var delay = _jitter.DecorrelatedJitterBackoffV2(0); + + // Assert + Assert.That(delay.TotalMilliseconds, Is.GreaterThan(0)); + } + } + + private class RetryDelayTester + { + public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) + { + return retryDelay.GetDelay(attemptNumber); + } + } } } diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 3a856126..01b67b72 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -828,6 +828,30 @@ public void Should_Rethrow_Or_Handle_If_ProcessorCreated_With_ThrowIfErrorFilter } } + [Test] + public void Should_Rethrow_With_ThrowIfErrorFilterUnsatisfied_True_ForExecuteWithParam() + { + var proc = new SimplePolicyProcessor(true).ExcludeError(); + var exc = Assert.Throws(() => ((SimplePolicyProcessor)proc).Execute(ActionWithParamWithInner, 1)); + Assert.That(exc.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY], Is.True); + } + + [Test] + public void Should_Rethrow_With_ThrowIfErrorFilterUnsatisfied_True_ForExecuteAsyncTWithParam() + { + var proc = new SimplePolicyProcessor(true).ExcludeError(); + var exc = Assert.ThrowsAsync(async () => await ((SimplePolicyProcessor)proc).ExecuteAsync(AsyncFuncWithParamWithInnerT, 1)); + Assert.That(exc.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY], Is.True); + } + + [Test] + public void Should_Rethrow_With_ThrowIfErrorFilterUnsatisfied_True_ForExecuteTWithParam() + { + var proc = new SimplePolicyProcessor(true).ExcludeError(); + var exc = Assert.Throws(() => ((SimplePolicyProcessor)proc).Execute(FuncWithParamWithInner, 1)); + Assert.That(exc.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY], Is.True); + } + [Test] [TestCase(true)] [TestCase(false)] @@ -1265,6 +1289,160 @@ void action(Exception _, ProcessingErrorInfo pi) Assert.That(result.IsSuccess, Is.True); } + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void Should_Execute_Func_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new SimplePolicyProcessor(); + + PolicyResult pr; + + if (waitAll) + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + } + else + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void Should_Execute_Func_WithParam_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new SimplePolicyProcessor(); + + PolicyResult pr; + + if (waitAll) + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithParamWithTaskWaitAll(cts, canceledOnLinkedSource), 1, cts.Token); + } + else + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithParamWithTaskWait(cts, canceledOnLinkedSource), 1, cts.Token); + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void Should_Execute_ActionWithParam_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new SimplePolicyProcessor(); + + PolicyResult pr; + + if (waitAll) + { + pr = processor.Execute(TaskWaitingDelegates.GetActionWithParamWithTaskWaitAll(cts, canceledOnLinkedSource), 4, cts.Token); + } + else + { + pr = processor.Execute(TaskWaitingDelegates.GetActionWithParamWithTaskWait(cts, canceledOnLinkedSource), 4, cts.Token); + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + + [Test] + [TestCase(true, true, true)] + [TestCase(false, true, true)] + [TestCase(true, false, true)] + [TestCase(false, false, true)] + [TestCase(true, true, false)] + [TestCase(false, true, false)] + [TestCase(true, false, false)] + [TestCase(false, false, false)] + public void Should_Execute_Action_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll, bool withContext) + { + using (var cts = new CancellationTokenSource()) + { + var processor = new SimplePolicyProcessor(); + + PolicyResult pr; + + if (waitAll) + { + if (withContext) + { + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), 5, cts.Token); + } + else + { + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + } + } + else + { + if (withContext) + { + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), 5, cts.Token); + } + else + { + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + } + } + Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + if (canceledOnLinkedSource) + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.PolicyCanceledError, Is.TypeOf()); + } + } + } + private class TestErrorProcessor : IErrorProcessor { public Exception Process(Exception error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, CancellationToken cancellationToken = default) diff --git a/tests/SyncErrorProcessorTParamTests.cs b/tests/SyncErrorProcessorTParamTests.cs index 31973b47..3b2e051f 100644 --- a/tests/SyncErrorProcessorTParamTests.cs +++ b/tests/SyncErrorProcessorTParamTests.cs @@ -184,7 +184,7 @@ public void Should_Process_By_BulkErrorProcessor() var errProcessor = new LogErrorProcessorWithParam(logger); var bp = new BulkErrorProcessor().WithErrorProcessor(errProcessor); - var _ = bp.Process(exception, new ProcessingErrorContext(PolicyAlias.NotSet, 5)); + bp.Process(exception, new ProcessingErrorContext(PolicyAlias.NotSet, 5)); Assert.That(logger.LastLoggedException, Is.SameAs(exception)); Assert.That(logger.Param, Is.EqualTo(5)); } @@ -198,7 +198,7 @@ public async Task Should_ProcessAsync_By_BulkErrorProcessor() var errProcessor = new LogErrorProcessorWithParam(logger); var bp = new BulkErrorProcessor().WithErrorProcessor(errProcessor); - var _ = await bp.ProcessAsync(exception, new ProcessingErrorContext(PolicyAlias.NotSet, 5)); + await bp.ProcessAsync(exception, new ProcessingErrorContext(PolicyAlias.NotSet, 5)); Assert.That(logger.LastLoggedException, Is.SameAs(exception)); Assert.That(logger.Param, Is.EqualTo(5)); } diff --git a/tests/SyncTypedErrorProcessor.cs b/tests/SyncTypedErrorProcessor.cs new file mode 100644 index 00000000..e18788c0 --- /dev/null +++ b/tests/SyncTypedErrorProcessor.cs @@ -0,0 +1,280 @@ +using NUnit.Framework; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoliNorError.Tests +{ + [TestFixture] + internal class SyncTypedErrorProcessor + { +#pragma warning disable RCS1194 // Implement exception constructors. +#pragma warning disable S3871 // Exception types should be "public" + private class TestException : Exception +#pragma warning restore S3871 // Exception types should be "public" +#pragma warning restore RCS1194 // Implement exception constructors. + { + public TestException(string message) : base(message) { } + + public string TestProperty { get; set; } + } + + private class TestTypedErrorProcessor : TypedErrorProcessor + { + public int ExecuteCallCount { get; private set; } + public TestException LastException { get; private set; } + public ProcessingErrorInfo LastProcessingErrorInfo { get; private set; } + public CancellationToken LastCancellationToken { get; private set; } + + public override void Execute(TestException error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, CancellationToken token = default) + { + ExecuteCallCount++; + LastException = error; + LastProcessingErrorInfo = catchBlockProcessErrorInfo; + LastCancellationToken = token; + error.TestProperty = nameof(error.TestProperty); + } + } + + [Test] + public void Should_CallExecute_WhenProcessIsCalled() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + var result = processor.Process(exception); + + // Assert + Assert.That(processor.ExecuteCallCount, Is.EqualTo(1)); + Assert.That(processor.LastException, Is.SameAs(exception)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + Assert.That(result, Is.SameAs(exception)); + } + + [Test] + public void Should_ReturnSameException_WhenProcessIsCalled() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + var result = processor.Process(exception); + + // Assert + Assert.That(result, Is.SameAs(exception)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public void Should_PassProcessingErrorInfo_WhenProvided() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + var errorInfo = new ProcessingErrorInfo(new ProcessingErrorContext()); + + // Act + processor.Process(exception, errorInfo); + + // Assert + Assert.That(processor.LastProcessingErrorInfo, Is.SameAs(errorInfo)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public void Should_PassCancellationToken_WhenProvided() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + using (var cts = new CancellationTokenSource()) + { + // Act + processor.Process(exception, null, cts.Token); + + // Assert + Assert.That(processor.LastCancellationToken, Is.EqualTo(cts.Token)); + } + } + + [Test] + public void Should_PassDefaultCancellationToken_WhenNotProvided() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + processor.Process(exception); + + // Assert + Assert.That(processor.LastCancellationToken, Is.EqualTo(default(CancellationToken))); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public async Task Should_CallExecute_WhenProcessAsyncIsCalled() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + var result = await processor.ProcessAsync(exception); + + // Assert + Assert.That(processor.ExecuteCallCount, Is.EqualTo(1)); + Assert.That(processor.LastException, Is.SameAs(exception)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + Assert.That(result, Is.SameAs(exception)); + } + + [Test] + public async Task Should_ReturnSameException_WhenProcessAsyncIsCalled() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + var result = await processor.ProcessAsync(exception); + + // Assert + Assert.That(result, Is.SameAs(exception)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public async Task Should_PassProcessingErrorInfo_WhenProvidedToProcessAsync() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + var errorInfo = new ProcessingErrorInfo(new ProcessingErrorContext()); + + // Act + await processor.ProcessAsync(exception, errorInfo); + + // Assert + Assert.That(processor.LastProcessingErrorInfo, Is.SameAs(errorInfo)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public async Task Should_PassCancellationToken_WhenProvidedToProcessAsync() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + using (var cts = new CancellationTokenSource()) + { + // Act + await processor.ProcessAsync(exception, null, false, cts.Token); + + // Assert + Assert.That(processor.LastCancellationToken, Is.EqualTo(cts.Token)); + } + } + + [Test] + public async Task Should_CompleteSuccessfully_WhenProcessAsyncReturnsTask() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + var task = processor.ProcessAsync(exception); + + // Assert + Assert.That(task, Is.Not.Null); + Assert.That(task.IsCompleted || await Task.WhenAny(task, Task.Delay(100)) == task, Is.True); + Assert.That(task.Result, Is.SameAs(exception)); + } + + [Test] + public void Should_HandleMultipleProcessCalls_Independently() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception1 = new TestException("error 1"); + var exception2 = new TestException("error 2"); + + // Act + processor.Process(exception1); + processor.Process(exception2); + + // Assert + Assert.That(processor.ExecuteCallCount, Is.EqualTo(2)); + Assert.That(processor.LastException, Is.SameAs(exception2)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public async Task Should_HandleMultipleProcessAsyncCalls_Independently() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception1 = new TestException("error 1"); + var exception2 = new TestException("error 2"); + + // Act + await processor.ProcessAsync(exception1); + await processor.ProcessAsync(exception2); + + // Assert + Assert.That(processor.ExecuteCallCount, Is.EqualTo(2)); + Assert.That(processor.LastException, Is.SameAs(exception2)); + Assert.That(processor.LastException.TestProperty, Is.EqualTo(nameof(TestException.TestProperty))); + } + + [Test] + public void Should_ProcessNullProcessingErrorInfo_WhenNotProvided() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act + processor.Process(exception, null); + + // Assert + Assert.That(processor.LastProcessingErrorInfo, Is.Null); + } + + [Test] + public void Should_AllowConfigAwaitParameter_InAsyncProcess() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + var exception = new TestException("test error"); + + // Act & Assert - should not throw + Assert.DoesNotThrowAsync(async () => await processor.ProcessAsync(exception, null, true)); + Assert.DoesNotThrowAsync(async () => await processor.ProcessAsync(exception, null, false)); + } + + [Test] + public void Should_ImplementIErrorProcessor_Interface() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + + // Assert + Assert.That(processor, Is.InstanceOf()); + } + + [Test] + public void Should_InheritFromTypedErrorProcessorBase() + { + // Arrange + var processor = new TestTypedErrorProcessor(); + + // Assert + Assert.That(processor, Is.InstanceOf>()); + } + } +} diff --git a/tests/TaskWaitingDelegates.cs b/tests/TaskWaitingDelegates.cs new file mode 100644 index 00000000..4ca3cfe0 --- /dev/null +++ b/tests/TaskWaitingDelegates.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoliNorError.Tests +{ + internal static class TaskWaitingDelegates + { + public static Func GetFuncWithParamWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return (_) => { GetActionWithTaskWait(sourceThatWillBeCanceled, canceledOnLinkedSource)(); return 1; }; + } + + public static Func GetFuncWithParamWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return (_) => { GetActionWithTaskWaitAll(sourceThatWillBeCanceled, canceledOnLinkedSource)(); return 1; }; + } + + public static Func GetFuncWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => { GetActionWithTaskWait(sourceThatWillBeCanceled, canceledOnLinkedSource)(); return 1; }; + } + + public static Func GetFuncWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => { GetActionWithTaskWaitAll(sourceThatWillBeCanceled, canceledOnLinkedSource)(); return 1; }; + } + + public static Action GetActionWithParamWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return (_) => GetActionWithTaskWait(sourceThatWillBeCanceled, canceledOnLinkedSource)(); + } + + public static Action GetActionWithParamWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return (_) => GetActionWithTaskWaitAll(sourceThatWillBeCanceled, canceledOnLinkedSource)(); + } + + public static Action GetActionWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => + { + using (var ctsOther = new CancellationTokenSource()) + { + var otherTask = Task.Run(async () => + { + await Task.Delay(1); + ctsOther.Cancel(); + ctsOther.Token.ThrowIfCancellationRequested(); + }); + + Task.WaitAll(otherTask, + GetTaskThatCanBeThrowOnLinkedToken(sourceThatWillBeCanceled, + canceledOnLinkedSource)); + } + }; + } + + public static Action GetActionWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => + { + if (canceledOnLinkedSource) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) + { + var innerToken = cancelTokenSource.Token; + GetCanceledTask(innerToken, sourceThatWillBeCanceled).Wait(); + } + } + else + { + GetCanceledTask(sourceThatWillBeCanceled.Token, sourceThatWillBeCanceled).Wait(); + } + }; + } + + private async static Task GetCanceledTask(CancellationToken tokenThatThrow, CancellationTokenSource sourceThatWillBeCanceled) + { + await Task.Delay(1); + sourceThatWillBeCanceled.Cancel(); + tokenThatThrow.ThrowIfCancellationRequested(); + } + + private async static Task GetTaskThatCanBeThrowOnLinkedToken(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + await Task.Delay(1); + if (canceledOnLinkedSource) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) + { + var innerToken = cancelTokenSource.Token; + sourceThatWillBeCanceled.Cancel(); + innerToken.ThrowIfCancellationRequested(); + } + } + else + { + sourceThatWillBeCanceled.Cancel(); + sourceThatWillBeCanceled.Token.ThrowIfCancellationRequested(); + } + } + } +} diff --git a/tests/WrappedPolicyTests.cs b/tests/WrappedPolicyTests.cs index cd30c99f..81277297 100644 --- a/tests/WrappedPolicyTests.cs +++ b/tests/WrappedPolicyTests.cs @@ -943,6 +943,41 @@ public void Should_Return_Correct_PolicyResult_When_OperationCanceledException_I } } + [Test] + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void Should_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token_In_Wrapped_Policy(bool canceledOnLinkedSource, bool waitAll) + { + using (var cts = new CancellationTokenSource()) + { + var policy = new RetryPolicy(1); + policy.WrapPolicy(new SimplePolicy()); + + PolicyResult pr; + + if (waitAll) + { + pr = policy.Handle(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + } + else + { + pr = policy.Handle(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + } Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); + Assert.That(pr.IsFailed, Is.True); + Assert.That(pr.WrappedPolicyResults.FirstOrDefault().Result.PolicyCanceledError, Is.Not.Null); + if (canceledOnLinkedSource) + { + Assert.That(pr.WrappedPolicyResults.FirstOrDefault().Result.PolicyCanceledError, Is.TypeOf()); + } + else + { + Assert.That(pr.WrappedPolicyResults.FirstOrDefault().Result.PolicyCanceledError, Is.TypeOf()); + } + } + } + private class AlwaysFailedAndCanceledSimplePolicyProcessor : ISimplePolicyProcessor { private readonly bool _setCanceledExcepton; diff --git a/tests/packages.config b/tests/packages.config index c7f79049..a1e4e416 100644 --- a/tests/packages.config +++ b/tests/packages.config @@ -2,12 +2,12 @@ - + - + \ No newline at end of file