From 07f20fc5ae06b9fa1a5aee65838b06fedef5c578 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 25 Dec 2025 14:18:04 +0300 Subject: [PATCH 001/109] Introduced the `PipelineResult` class. --- src/FuncPipeline/PipelineResult.cs | 42 +++++ tests/PipelineResultTests.cs | 239 +++++++++++++++++++++++++++++ tests/PoliNorError.Tests.csproj | 1 + 3 files changed, 282 insertions(+) create mode 100644 src/FuncPipeline/PipelineResult.cs create mode 100644 tests/PipelineResultTests.cs diff --git a/src/FuncPipeline/PipelineResult.cs b/src/FuncPipeline/PipelineResult.cs new file mode 100644 index 00000000..6fa844a0 --- /dev/null +++ b/src/FuncPipeline/PipelineResult.cs @@ -0,0 +1,42 @@ +namespace PoliNorError +{ + public sealed class PipelineResult + { + internal static PipelineResult Failure(PolicyResult failedPolicyResult) + { + return new PipelineResult() + { + FailedPolicyResult = failedPolicyResult, + IsCanceled = failedPolicyResult.IsCanceled + }; + } + + internal static PipelineResult Failure(PolicyResult failedPolicyResult, bool isCanceled) + { + return new PipelineResult() + { + FailedPolicyResult = failedPolicyResult, + IsCanceled = isCanceled + }; + } + + internal static PipelineResult Success(PolicyResult successPolicyResult) + { + return new PipelineResult() + { + SucceededPolicyResult = successPolicyResult, + }; + } + + private PipelineResult() { } + + internal PolicyResult FailedPolicyResult { get; private set; } + internal PolicyResult SucceededPolicyResult { get; private set; } + + public bool IsFailed => SucceededPolicyResult is null; + + public bool IsCanceled { get; private set; } + + public T Result => IsFailed ? default : SucceededPolicyResult.Result; + } +} diff --git a/tests/PipelineResultTests.cs b/tests/PipelineResultTests.cs new file mode 100644 index 00000000..1308e01a --- /dev/null +++ b/tests/PipelineResultTests.cs @@ -0,0 +1,239 @@ +using NUnit.Framework; + +namespace PoliNorError.Tests +{ + public class PipelineResultTests + { + [Test] + public void Should_Create_Failed_PipelineResult_With_PolicyResult() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailed(); + + // Act + var result = PipelineResult.Failure(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.True); + Assert.That(result.IsCanceled, Is.False); + Assert.That(result.Result, Is.EqualTo(default(int))); + } + + [Test] + public void Should_Create_Failed_PipelineResult_With_Canceled_PolicyResult() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailedAndCanceled(); + + // Act + var result = PipelineResult.Failure(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.True); + Assert.That(result.IsCanceled, Is.True); + Assert.That(result.Result, Is.Null); + } + + [Test] + public void Should_Create_Failed_PipelineResult_With_Explicit_Cancellation_Flag() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailed(); + + // Act + var result = PipelineResult.Failure(policyResult, isCanceled: true); + + // Assert + Assert.That(result.IsFailed, Is.True); + Assert.That(result.IsCanceled, Is.True); + Assert.That(result.Result, Is.EqualTo(default(double))); + } + + [Test] + public void Should_Create_Failed_PipelineResult_With_IsCanceled_False_When_Explicitly_Set() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailedAndCanceled(); + + // Act + var result = PipelineResult.Failure(policyResult, isCanceled: false); + + // Assert + Assert.That(result.IsFailed, Is.True); + Assert.That(result.IsCanceled, Is.False); + } + + [Test] + public void Should_Create_Success_PipelineResult_With_Result_Value() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetOk(); + policyResult.SetResult(42); + + // Act + var result = PipelineResult.Success(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.False); + Assert.That(result.IsCanceled, Is.False); + Assert.That(result.Result, Is.EqualTo(42)); + } + + [Test] + public void Should_Create_Success_PipelineResult_With_Reference_Type() + { + // Arrange + var expectedValue = "test string"; + var policyResult = new PolicyResult(); + policyResult.SetOk(); + policyResult.SetResult(expectedValue); + + // Act + var result = PipelineResult.Success(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.False); + Assert.That(result.Result, Is.EqualTo(expectedValue)); + } + + [Test] + public void Should_Return_Default_Result_When_Failed() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailed(); + + // Act + var result = PipelineResult.Failure(policyResult); + + // Assert + Assert.That(result.Result, Is.EqualTo(0)); + } + + [Test] + public void Should_Return_Null_For_Reference_Type_When_Failed() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailed(); + + // Act + var result = PipelineResult.Failure(policyResult); + + // Assert + Assert.That(result.Result, Is.Null); + } + + [Test] + public void Should_Have_IsFailed_True_When_SucceededPolicyResult_Is_Null() + { + // Arrange + var policyResult = new PolicyResult(); + + // Act + var result = PipelineResult.Failure(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.True); + } + + [Test] + public void Should_Have_IsFailed_False_When_SucceededPolicyResult_Is_Not_Null() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetResult(true); + + // Act + var result = PipelineResult.Success(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.False); + } + + [Test] + public void Should_Preserve_IsCanceled_From_PolicyResult() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetCanceled(); + + // Act + var result = PipelineResult.Failure(policyResult); + + // Assert + Assert.That(result.IsCanceled, Is.True); + } + + [Test] + public void Should_Not_Set_IsCanceled_For_Success_Result() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetResult(100); + + // Act + var result = PipelineResult.Success(policyResult); + + // Assert + Assert.That(result.IsCanceled, Is.False); + } + + [Test] + public void Should_Handle_Nullable_Value_Type() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetResult(null); + + // Act + var result = PipelineResult.Success(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.False); + Assert.That(result.Result, Is.Null); + } + + [Test] + public void Should_Return_Correct_Result_For_Custom_Object() + { + // Arrange + var customObject = new TestClass { Value = "test" }; + var policyResult = new PolicyResult(); + policyResult.SetResult(customObject); + + // Act + var result = PipelineResult.Success(policyResult); + + // Assert + Assert.That(result.IsFailed, Is.False); + Assert.That(result.Result, Is.SameAs(customObject)); + Assert.That(result.Result.Value, Is.EqualTo("test")); + } + + [Test] + public void Should_Override_PolicyResult_IsCanceled_When_Explicitly_Provided() + { + // Arrange + var policyResult = new PolicyResult(); + policyResult.SetFailed(); + // Note: policyResult.IsCanceled is false + + // Act + var result = PipelineResult.Failure(policyResult, isCanceled: true); + + // Assert + Assert.That(result.IsCanceled, Is.True); + } + + private class TestClass + { + public string Value { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index bfff3372..d316245a 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -113,6 +113,7 @@ + From 15f657426259ab0058a3c6acda6905b3998f1fbc Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 26 Dec 2025 17:03:47 +0300 Subject: [PATCH 002/109] Introduced the internal `Func.ToHandledByPolicy` extension method for the pipelined function builder. --- src/FuncPipeline/PipelineFuncExtensions.cs | 27 ++++++++++++ tests/PipelineFuncExtensionsTests.cs | 50 ++++++++++++++++++++++ tests/PoliNorError.Tests.csproj | 1 + 3 files changed, 78 insertions(+) create mode 100644 src/FuncPipeline/PipelineFuncExtensions.cs create mode 100644 tests/PipelineFuncExtensionsTests.cs diff --git a/src/FuncPipeline/PipelineFuncExtensions.cs b/src/FuncPipeline/PipelineFuncExtensions.cs new file mode 100644 index 00000000..483f6c0f --- /dev/null +++ b/src/FuncPipeline/PipelineFuncExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; + +namespace PoliNorError +{ + internal static class PipelineFuncExtensions + { + internal static Func> ToHandledByPolicy(this Func func, SimplePolicy policy) + { + return (t, ct) => + { + var res = policy.Handle(func, t, ct); + + if (ct.IsCancellationRequested) + { + return PipelineResult.Failure(res, true); + } + + if (!res.NoError || res.IsCanceled) + { + return PipelineResult.Failure(res); + } + return PipelineResult.Success(res); + }; + } + } +} diff --git a/tests/PipelineFuncExtensionsTests.cs b/tests/PipelineFuncExtensionsTests.cs new file mode 100644 index 00000000..bf1d8390 --- /dev/null +++ b/tests/PipelineFuncExtensionsTests.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using System; +using System.Threading; + +namespace PoliNorError.Tests +{ + public class PipelineFuncExtensionsTests + { + [Test] + public void Should_Return_Success_When_Policy_Handles_Successfully_And_No_Cancellation() + { + // Arrange + Func func = x => x.ToString(); + var policy = new SimplePolicy(); + var cancellationToken = CancellationToken.None; + + // Act + var wrappedFunc = func.ToHandledByPolicy(policy); + var result = wrappedFunc(42, cancellationToken); + + // Assert + Assert.That(result.IsFailed, Is.False); + Assert.That(result.IsCanceled, Is.False); + Assert.That(result.Result, Is.EqualTo("42")); + Assert.That(result.SucceededPolicyResult, Is.Not.Null); + } + + [Test] + public void Should_Return_Failure_When_CancellationToken_Is_Canceled() + { + // Arrange + Func func = x => x.ToString(); + var policy = new SimplePolicy(); + using (var cts = new CancellationTokenSource()) + { + cts.Cancel(); + + // Act + var wrappedFunc = func.ToHandledByPolicy(policy); + var result = wrappedFunc(42, cts.Token); + + // Assert + Assert.That(result.IsFailed, Is.True); + Assert.That(result.IsCanceled, Is.True); + Assert.That(result.FailedPolicyResult, Is.Not.Null); + Assert.That(result.SucceededPolicyResult, Is.Null); + } + } + } +} diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index d316245a..86888545 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -113,6 +113,7 @@ + From 8b084e52956de3e268b4e02a33e0e2d4e8694208 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 27 Dec 2025 20:02:58 +0300 Subject: [PATCH 003/109] DRY refactor `SimplePolicyProcessor.Execute` catch block. --- src/Simple/SimplePolicyProcessor.cs | 43 +++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 6f05b264..fec64226 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -93,19 +93,16 @@ private PolicyResult Execute(Action action, TParam param, EmptyE } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var (ShouldRethrow, HasFilterException) = ProcessErrorFilter(ex, result); + if (ShouldRethrow) { - 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; + } + + if (HasFilterException) + { + return result; } exHandler.Handle(ex, emptyErrorContext); @@ -596,5 +593,27 @@ public SimplePolicyProcessor AddErrorFilter(Func Date: Sun, 28 Dec 2025 15:39:48 +0300 Subject: [PATCH 004/109] Inline internal `IPolicyProcessor.AddExcludedError` and `AddIncludedError` extensions. --- src/PolicyProcessorErrorFiltering.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) 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); - } } } From b953e6569beb5900db1b36bb768d7787e5d5a5b2 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 29 Dec 2025 16:57:12 +0300 Subject: [PATCH 005/109] Further DRY refactor the `SimplePolicyProcessor.Execute` catch block. --- src/Simple/SimplePolicyProcessor.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index fec64226..e7c2fb4c 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -93,19 +93,17 @@ private PolicyResult Execute(Action action, TParam param, EmptyE } catch (Exception ex) { - var (ShouldRethrow, HasFilterException) = ProcessErrorFilter(ex, result); - if (ShouldRethrow) + var (shouldRethrow, hasFilterException) = ProcessErrorFilter(ex, result); + if (shouldRethrow) { ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; throw; } - if (HasFilterException) + if (!hasFilterException) { - return result; + exHandler.Handle(ex, emptyErrorContext); } - - exHandler.Handle(ex, emptyErrorContext); } return result; } @@ -605,14 +603,14 @@ public SimplePolicyProcessor AddErrorFilter(Func Date: Tue, 30 Dec 2025 11:57:28 +0300 Subject: [PATCH 006/109] Defer allocation of the catch-block handler class until an exception occurs in `SimplePolicyProcessor.Execute`. --- src/Simple/SimplePolicyProcessor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index e7c2fb4c..1018b476 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -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); @@ -102,6 +100,7 @@ private PolicyResult Execute(Action action, TParam param, EmptyE if (!hasFilterException) { + var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); exHandler.Handle(ex, emptyErrorContext); } } From a4ae49f194ea4d18950ace3116daa91b048e0656 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 1 Jan 2026 13:36:29 +0300 Subject: [PATCH 007/109] Refactored internal `PolicyProcessorCatchBlockSyncHandler` to use `ShouldHandleException` instead of `PreHandle`, replacing the tuple return value with a `HandleCatchBlockResult` enum. --- .../PolicyProcessorCatchBlockAsyncHandler.cs | 8 ++++---- .../PolicyProcessorCatchBlockHandlerBase.cs | 9 +++++++++ .../PolicyProcessorCatchBlockSyncHandler.cs | 8 ++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs index 3506dab1..f2926464 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs @@ -16,13 +16,13 @@ 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); + return PostHandle(bulkProcessResult, shouldHandleResult); } } } diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs index f6878cdd..04ffd2d1 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs @@ -22,6 +22,15 @@ 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); + } + protected (HandleCatchBlockResult Result, bool CanProcess) PreHandle(Exception ex, ErrorContext errorContext) { if (_cancellationToken.IsCancellationRequested) diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs index 02cc201d..841db1b8 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs @@ -12,13 +12,13 @@ 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); + return PostHandle(bulkProcessResult, shouldHandleResult); } } } From a6f2331bb4224317c5271cb2ee24556ed474137c Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 1 Jan 2026 14:31:09 +0300 Subject: [PATCH 008/109] Deprecate the `PolicyProcessorCatchBlockHandlerBase.PreHandle` method. --- src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs index 04ffd2d1..3e43a06f 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs @@ -31,6 +31,9 @@ protected HandleCatchBlockResult ShouldHandleException(Exception ex, ErrorContex 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) From 5b9a5bb803bf99e17a5bc9eadccf456d101e5ec8 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 1 Jan 2026 20:32:09 +0300 Subject: [PATCH 009/109] Materialize `BulkProcessResult.ProcessErrors` once and replace LINQ `Select` with iterator in `ToCatchBlockExceptions` method to avoid re-enumeration and extra allocations. --- src/ErrorProcessors/BulkErrorProcessor.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ErrorProcessors/BulkErrorProcessor.cs b/src/ErrorProcessors/BulkErrorProcessor.cs index c9ede63a..f358fa76 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,12 @@ 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 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 +269,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); + } } } } From 4cc779c6ba854dd42547e9bff82d6448fbd35401 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 2 Jan 2026 16:47:20 +0300 Subject: [PATCH 010/109] Add the `BulkProcessResult.HasProcessErrors` property and tests for the `BulkProcessResult` class. --- src/ErrorProcessors/BulkErrorProcessor.cs | 5 + tests/BulkProcessResultTests.cs | 205 ++++++++++++++++++++++ tests/PoliNorError.Tests.csproj | 1 + 3 files changed, 211 insertions(+) create mode 100644 tests/BulkProcessResultTests.cs diff --git a/src/ErrorProcessors/BulkErrorProcessor.cs b/src/ErrorProcessors/BulkErrorProcessor.cs index f358fa76..a49ddb00 100644 --- a/src/ErrorProcessors/BulkErrorProcessor.cs +++ b/src/ErrorProcessors/BulkErrorProcessor.cs @@ -258,6 +258,11 @@ public BulkProcessResult(Exception handlingError, IEnumerable 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. /// 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/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 86888545..63f0d4e4 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -90,6 +90,7 @@ + From d1bdc1d7f41d977887bf67cf79b3e60c04bac3b7 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 3 Jan 2026 08:13:05 +0300 Subject: [PATCH 011/109] Guard the call to `BulkProcessResult.ToCatchBlockExceptions` with `BulkProcessResult.HasProcessErrors` in `PolicyResult.AddBulkProcessorErrors`. --- src/PolicyResultExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) From 4fff8bb87d7c9c0a7c1db53a0e4761b527d24b5b Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 3 Jan 2026 17:48:04 +0300 Subject: [PATCH 012/109] Inline the protected `PolicyProcessorCatchBlockHandlerBase.PostHandle` method. --- .../PolicyProcessorCatchBlockAsyncHandler.cs | 3 ++- src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs index f2926464..e3b2c384 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockAsyncHandler.cs @@ -22,7 +22,8 @@ public async Task HandleAsync(Exception ex, ErrorContext var bulkProcessResult = await _bulkErrorProcessor.ProcessAsync(ex, errorContext.ToProcessingErrorContext(), _configAwait, _cancellationToken).ConfigureAwait(_configAwait); - return PostHandle(bulkProcessResult, shouldHandleResult); + _policyResult.AddBulkProcessorErrors(bulkProcessResult); + return bulkProcessResult.IsCanceled ? HandleCatchBlockResult.Canceled : shouldHandleResult; } } } diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs index 841db1b8..f9fefc7f 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockSyncHandler.cs @@ -18,7 +18,8 @@ public HandleCatchBlockResult Handle(Exception ex, ErrorContext errorContext var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), _cancellationToken); - return PostHandle(bulkProcessResult, shouldHandleResult); + _policyResult.AddBulkProcessorErrors(bulkProcessResult); + return bulkProcessResult.IsCanceled ? HandleCatchBlockResult.Canceled : shouldHandleResult; } } } From 55ec19f80b10261fc25d380b70c20e2a2e44ab84 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 3 Jan 2026 17:50:55 +0300 Subject: [PATCH 013/109] Deprecate the `PolicyProcessorCatchBlockHandlerBase.PostHandle` method. --- src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs index 3e43a06f..f116ec89 100644 --- a/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs +++ b/src/CatchBlockHandlers/PolicyProcessorCatchBlockHandlerBase.cs @@ -57,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); From 1a0e5ae79424eb21f4827b359f00c4af934e3384 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 4 Jan 2026 19:08:09 +0300 Subject: [PATCH 014/109] Make the `ErrorContext` class public (previously internal). --- src/CatchBlockHandlers/ErrorContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 14d12bfe9918a44637d350af3e83eb6c9c53ec20 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 5 Jan 2026 18:36:45 +0300 Subject: [PATCH 015/109] Follow up: further DRY refactor `SimplePolicyProcessor.Execute` catch block. --- src/Simple/SimplePolicyProcessor.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 1018b476..01d96649 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -597,19 +597,14 @@ public SimplePolicyProcessor AddErrorFilter(Func Date: Tue, 6 Jan 2026 20:58:48 +0300 Subject: [PATCH 016/109] Added the protected internal `PolicyProcessor.HandleException` method and the `ExceptionHandlingBehavior` and `ExceptionHandlingResult` enums. --- src/PolicyProcessor.cs | 115 +++++++ tests/PoliNorError.Tests.csproj | 1 + .../PolicyProcessor.HandleException.Tests.cs | 285 ++++++++++++++++++ 3 files changed, 401 insertions(+) create mode 100644 tests/PolicyProcessor.HandleException.Tests.cs diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index a4b836b0..d290db54 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -72,7 +72,122 @@ internal PolicyProcessorCatchBlockAsyncHandler GetCatchBlockAsyncHandler(P policyRuleFunc); } + protected internal ExceptionHandlingResult HandleException(Exception ex, PolicyResult policyResult, ErrorContext errorContext, CancellationToken token, Func, bool> policyRuleFunc = null, ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle) + { + if (handlingBehavior != ExceptionHandlingBehavior.ConditionalRethrow) + { + policyResult.AddError(ex); + } + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + + var (result, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); + if (result == HandleCatchBlockResult.FailedByErrorFilter) + { + if (!(error is null)) + { + if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + policyResult.AddError(ex); + } + policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.ErrorFilter, true)); + policyResult.SetFailedAndFilterUnsatisfied(); + return ExceptionHandlingResult.Handled; + } + + switch (handlingBehavior) + { + case ExceptionHandlingBehavior.ConditionalRethrow: + return ExceptionHandlingResult.Rethrow; + case ExceptionHandlingBehavior.Handle: + policyResult.SetFailedAndFilterUnsatisfied(); + return ExceptionHandlingResult.Handled; + default: + policyResult.AddCatchBlockError(new CatchBlockException(new ArgumentOutOfRangeException(nameof(handlingBehavior), handlingBehavior, null), ex, CatchBlockExceptionSource.ErrorFilter, true)); + policyResult.SetFailedAndFilterUnsatisfied(); + return ExceptionHandlingResult.Handled; + } + } + + if (result == HandleCatchBlockResult.FailedByPolicyRules) + { + policyResult.SetFailedInner(); + return ExceptionHandlingResult.Handled; + } + + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + if (bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + } + return ExceptionHandlingResult.Handled; + } + + private (HandleCatchBlockResult, Exception) ShouldHandleException(Exception ex, ErrorContext errorContext, Func, bool> policyRuleFunc = null) + { + var (filterPassed, error) = RunErrorFilterFunc(); + + if (!filterPassed) + return (HandleCatchBlockResult.FailedByErrorFilter, error); + + if (!(policyRuleFunc is null) && !policyRuleFunc(errorContext)) + return (HandleCatchBlockResult.FailedByPolicyRules, null); + + return (HandleCatchBlockResult.Success, null); + + (bool, Exception) RunErrorFilterFunc() + { + try + { + return (ErrorFilter.GetCanHandle()(ex), null); + } + catch (Exception exIn) + { + return (false, exIn); + } + } + } + public IEnumerator GetEnumerator() => _bulkErrorProcessor.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + /// + /// Represents the outcome of an exception handling. + /// + public enum ExceptionHandlingResult + { + /// + /// 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: + /// + Handle, + + /// + /// Handle the exception and rethrow ONLY if an error filter condition is NOT satisfied. + /// Outcome: (if filtered) + /// or (if unfiltered) + /// + ConditionalRethrow + } } diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 63f0d4e4..fbf1e9fb 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -118,6 +118,7 @@ + diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs new file mode 100644 index 00000000..7ff35c62 --- /dev/null +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -0,0 +1,285 @@ +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 class PolicyProcessorTests + { + private class TestPolicyProcessor : PolicyProcessor + { + public TestPolicyProcessor(IBulkErrorProcessor bulkErrorProcessor) + { + _bulkErrorProcessor = bulkErrorProcessor; + ErrorFilter.AddExcludedErrorFilter(); + } + + public ExceptionHandlingResult TestHandleException( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + CancellationToken token, + Func, bool> policyRuleFunc = null, + ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle) + { + return HandleException(ex, policyResult, errorContext, token, policyRuleFunc, handlingBehavior); + } + } + + 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 void AddProcessor(IErrorProcessor errorProcessor) => throw new NotImplementedException(); + public IEnumerator GetEnumerator() => throw new NotImplementedException(); + + public BulkProcessResult Process(Exception handlingError, ProcessingErrorContext errorContext = null, CancellationToken token = default) + { + return ResultToReturn ?? new BulkProcessResult(handlingError, null); + } + + public Task ProcessAsync(Exception handlingError, ProcessingErrorContext errorContext = null, bool configAwait = false, CancellationToken token = default) => throw new NotImplementedException(); + IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); + } + + [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); + } + } + + [Test] + public void Should_ReturnHandled_WhenExceptionIsInvalidOperationException() + { + // 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)); + } + + [Test] + public void Should_ReturnRethrow_WhenErrorFilterFailsAndBehaviorIsConditionalRethrow() + { + // Arrange + var bulkProcessor = new TestBulkErrorProcessor(); + var processor = new TestPolicyProcessor(bulkProcessor); + 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, + 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); + var policyResult = PolicyResult.ForSync(); + var errorContext = new TestErrorContext("test"); + var exception = new ArgumentException("test"); + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + null, + ExceptionHandlingBehavior.Handle); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + } + + [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 _) => false; + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + policyRuleFunc); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.IsFailed, Is.True); + } + + [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 _) => true; + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + policyRuleFunc); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + } + + [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); + } + + [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 + } + + [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); + } + } +} From 280f5017f2e9936188a3af31e79ea0d0042c6af6 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 7 Jan 2026 18:28:22 +0300 Subject: [PATCH 017/109] Refactor the `PolicyProcessor.HandleException` method --- src/PolicyProcessor.cs | 63 ++++++++++++++----- .../PolicyProcessor.HandleException.Tests.cs | 15 ++++- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index d290db54..7a52b0b6 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -72,18 +72,53 @@ internal PolicyProcessorCatchBlockAsyncHandler GetCatchBlockAsyncHandler(P policyRuleFunc); } - protected internal ExceptionHandlingResult HandleException(Exception ex, PolicyResult policyResult, ErrorContext errorContext, CancellationToken token, Func, bool> policyRuleFunc = null, ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle) + protected internal ExceptionHandlingResult HandleException( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Action, CancellationToken> saveError, + Func, bool> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + CancellationToken token) { + saveError = saveError ?? CreateDefaultErrorSaver(); if (handlingBehavior != ExceptionHandlingBehavior.ConditionalRethrow) { - policyResult.AddError(ex); + saveError(policyResult, ex, errorContext, token); + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } } - if (token.IsCancellationRequested) + + var result = DetermineExceptionHandlingResult(ex, policyResult, errorContext, saveError, policyRuleFunc, handlingBehavior, token); + if (result != ExceptionHandlingResult.Accepted) + { + return result; + } + + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + if (bulkProcessResult.IsCanceled) { policyResult.SetFailedAndCanceled(); - return ExceptionHandlingResult.Handled; } + return ExceptionHandlingResult.Handled; + } + internal static Action, CancellationToken> CreateDefaultErrorSaver() => + (pr, e, _, __) => pr.AddError(e); + + private ExceptionHandlingResult DetermineExceptionHandlingResult( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Action, CancellationToken> saveError, + Func, bool> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + CancellationToken token) + { var (result, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); if (result == HandleCatchBlockResult.FailedByErrorFilter) { @@ -91,7 +126,7 @@ protected internal ExceptionHandlingResult HandleException(Exception ex, Poli { if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) { - policyResult.AddError(ex); + saveError(policyResult, ex, errorContext, token); } policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.ErrorFilter, true)); policyResult.SetFailedAndFilterUnsatisfied(); @@ -111,20 +146,12 @@ protected internal ExceptionHandlingResult HandleException(Exception ex, Poli return ExceptionHandlingResult.Handled; } } - if (result == HandleCatchBlockResult.FailedByPolicyRules) { policyResult.SetFailedInner(); return ExceptionHandlingResult.Handled; } - - var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); - policyResult.AddBulkProcessorErrors(bulkProcessResult); - if (bulkProcessResult.IsCanceled) - { - policyResult.SetFailedAndCanceled(); - } - return ExceptionHandlingResult.Handled; + return ExceptionHandlingResult.Accepted; } private (HandleCatchBlockResult, Exception) ShouldHandleException(Exception ex, ErrorContext errorContext, Func, bool> policyRuleFunc = null) @@ -161,6 +188,11 @@ protected internal ExceptionHandlingResult HandleException(Exception ex, Poli /// 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. /// @@ -179,7 +211,8 @@ public enum ExceptionHandlingBehavior { /// /// Handle the exception and do not rethrow it. - /// Outcome: + /// Outcome: + /// or /// Handle, diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 7ff35c62..3470f9ae 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -29,7 +29,7 @@ public ExceptionHandlingResult TestHandleException( Func, bool> policyRuleFunc = null, ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle) { - return HandleException(ex, policyResult, errorContext, token, policyRuleFunc, handlingBehavior); + return HandleException(ex, policyResult, errorContext, null, policyRuleFunc, handlingBehavior, token); } } @@ -47,11 +47,14 @@ private class TestBulkErrorProcessor : IBulkErrorProcessor { public BulkProcessResult ResultToReturn { 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); } @@ -79,6 +82,7 @@ public void Should_ReturnHandled_AndSetFailedAndCanceled_WhenTokenIsCanceled() 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)); } } @@ -100,6 +104,7 @@ public void Should_ReturnHandled_WhenExceptionIsInvalidOperationException() // Assert Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); } [Test] @@ -150,6 +155,7 @@ public void Should_ReturnHandled_WhenErrorFilterFailsAndBehaviorIsHandle() // Assert Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); } [Test] @@ -174,6 +180,7 @@ public void Should_ReturnHandled_AndSetFailed_WhenPolicyRuleFuncReturnsFalse() // Assert Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); Assert.That(policyResult.IsFailed, Is.True); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); } [Test] @@ -200,6 +207,7 @@ public void Should_ReturnHandled_WhenPolicyRuleFuncReturnsTrue() // Assert Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); } [Test] @@ -228,6 +236,7 @@ public void Should_SetFailedAndCanceled_WhenBulkProcessResultIsCanceled() 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] @@ -256,6 +265,8 @@ public void Should_AddBulkProcessorErrors_WhenBulkProcessorReturnsErrors() // 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] @@ -280,6 +291,8 @@ public void Should_AddExceptionToPolicyResult_WhenHandlingException() // 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)); } } } From 96891d6cd781623c3650fb5dc57040ef47002924 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 8 Jan 2026 18:39:40 +0300 Subject: [PATCH 018/109] Add the internal static `PolicyProcessor.DefaultErrorSaver` property. --- src/PolicyProcessor.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 7a52b0b6..295f1e57 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -15,14 +15,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 +55,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(), @@ -107,6 +107,8 @@ protected internal ExceptionHandlingResult HandleException( return ExceptionHandlingResult.Handled; } + internal static Action, CancellationToken> DefaultErrorSaver { get; } = CreateDefaultErrorSaver(); + internal static Action, CancellationToken> CreateDefaultErrorSaver() => (pr, e, _, __) => pr.AddError(e); From 86c588461f0da2cee252cbe54116ad9574ba52c3 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 9 Jan 2026 19:48:44 +0300 Subject: [PATCH 019/109] Add internal `ErrorProcessingCancellationEffect` enum. --- src/PolicyProcessor.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 295f1e57..4f02b415 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -225,4 +225,32 @@ public enum ExceptionHandlingBehavior /// ConditionalRethrow } + + /// + /// Describes whether a cancellation that occurred during should be propagated to . + /// + internal 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 + } } From 484dcee9f660d35eef6979b41914ce08e6b5d205 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 10 Jan 2026 12:10:57 +0300 Subject: [PATCH 020/109] Make `ErrorProcessingCancellationEffect` public and use it in `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 5 +- .../PolicyProcessor.HandleException.Tests.cs | 53 ++++++++++++++----- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 4f02b415..b680fe71 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -79,6 +79,7 @@ protected internal ExceptionHandlingResult HandleException( Action, CancellationToken> saveError, Func, bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, + ErrorProcessingCancellationEffect cancellationEffect, CancellationToken token) { saveError = saveError ?? CreateDefaultErrorSaver(); @@ -100,7 +101,7 @@ protected internal ExceptionHandlingResult HandleException( var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); policyResult.AddBulkProcessorErrors(bulkProcessResult); - if (bulkProcessResult.IsCanceled) + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) { policyResult.SetFailedAndCanceled(); } @@ -229,7 +230,7 @@ public enum ExceptionHandlingBehavior /// /// Describes whether a cancellation that occurred during should be propagated to . /// - internal enum ErrorProcessingCancellationEffect + public enum ErrorProcessingCancellationEffect { /// /// Cancellation during error processing does not influence diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 3470f9ae..7fbee3f1 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -27,9 +27,10 @@ public ExceptionHandlingResult TestHandleException( ErrorContext errorContext, CancellationToken token, Func, bool> policyRuleFunc = null, - ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle) + ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate) { - return HandleException(ex, policyResult, errorContext, null, policyRuleFunc, handlingBehavior, token); + return HandleException(ex, policyResult, errorContext, null, policyRuleFunc, handlingBehavior, cancellationEffect, token); } } @@ -49,18 +50,18 @@ private class TestBulkErrorProcessor : IBulkErrorProcessor public bool IsProcessed { get; private set; } - public void AddProcessor(IErrorProcessor errorProcessor) => throw new NotImplementedException(); - public IEnumerator GetEnumerator() => throw new NotImplementedException(); + 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) + 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) => throw new NotImplementedException(); - IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); - } + public Task ProcessAsync(Exception handlingError, ProcessingErrorContext errorContext = null, bool configAwait = false, CancellationToken token = default) => throw new NotImplementedException(); + IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); + } [Test] public void Should_ReturnHandled_AndSetFailedAndCanceled_WhenTokenIsCanceled() @@ -71,12 +72,12 @@ public void Should_ReturnHandled_AndSetFailedAndCanceled_WhenTokenIsCanceled() var policyResult = PolicyResult.ForSync(); var errorContext = new TestErrorContext("test"); var exception = new Exception("test exception"); - using (var cts = new CancellationTokenSource()) - { - cts.Cancel(); + using (var cts = new CancellationTokenSource()) + { + cts.Cancel(); - // Act - var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token); + // Act + var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token); // Assert Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); @@ -86,6 +87,32 @@ public void Should_ReturnHandled_AndSetFailedAndCanceled_WhenTokenIsCanceled() } } + [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, 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_ReturnHandled_WhenExceptionIsInvalidOperationException() { From 3121fee7f53138c79d2f4900e6f74e6a90f12e3f Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 11 Jan 2026 19:51:16 +0300 Subject: [PATCH 021/109] Added the internal static `PolicyProcessor.CreateDefaultPolicyRule` method and the `PolicyProcessor.DefaultPolicyRule` property. --- src/PolicyProcessor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index b680fe71..34e5ab67 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -113,6 +113,11 @@ protected internal ExceptionHandlingResult HandleException( internal static Action, CancellationToken> CreateDefaultErrorSaver() => (pr, e, _, __) => pr.AddError(e); + internal static Func, bool> DefaultPolicyRule { get; } = CreateDefaultPolicyRule(); + + internal static Func, bool> CreateDefaultPolicyRule() => + (_) => true; + private ExceptionHandlingResult DetermineExceptionHandlingResult( Exception ex, PolicyResult policyResult, @@ -164,7 +169,7 @@ private ExceptionHandlingResult DetermineExceptionHandlingResult( if (!filterPassed) return (HandleCatchBlockResult.FailedByErrorFilter, error); - if (!(policyRuleFunc is null) && !policyRuleFunc(errorContext)) + if (policyRuleFunc?.Invoke(errorContext) == false) return (HandleCatchBlockResult.FailedByPolicyRules, null); return (HandleCatchBlockResult.Success, null); From 1e06090a8f785cfbb4d84961321b7b56a92d6f01 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 11 Jan 2026 20:03:21 +0300 Subject: [PATCH 022/109] DRY refactor catch-block handling in `SimplePolicyProcessor.Execute`. --- src/PolicyProcessor.cs | 2 +- src/Simple/SimplePolicyProcessor.cs | 35 ++++++++----------------- tests/ErrorWithInnerExcThrowingFuncs.cs | 3 +++ tests/SimplePolicyProcessorTests.cs | 8 ++++++ 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 34e5ab67..4ec7fb20 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -232,7 +232,7 @@ public enum ExceptionHandlingBehavior ConditionalRethrow } - /// + /// /// Describes whether a cancellation that occurred during should be propagated to . /// public enum ErrorProcessingCancellationEffect diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 01d96649..efbabeb5 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) { @@ -91,18 +91,12 @@ private PolicyResult Execute(Action action, TParam param, EmptyE } catch (Exception ex) { - var (shouldRethrow, hasFilterException) = ProcessErrorFilter(ex, result); - if (shouldRethrow) + var handlingResult = HandleException(ex, result, emptyErrorContext, token); + if (handlingResult == ExceptionHandlingResult.Rethrow) { ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; throw; } - - if (!hasFilterException) - { - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - exHandler.Handle(ex, emptyErrorContext); - } } return result; } @@ -591,21 +585,14 @@ public SimplePolicyProcessor AddErrorFilter(Func 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(); diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 3a856126..07fdd29a 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -828,6 +828,14 @@ 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] [TestCase(true)] [TestCase(false)] From 32f6938eaff6192e32d59c993add0900f75d309c Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 12 Jan 2026 17:27:44 +0300 Subject: [PATCH 023/109] Fix `PolicyProcessor.HandleException` method. --- src/PolicyProcessor.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 4ec7fb20..8a0e4fec 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -99,6 +99,16 @@ protected internal ExceptionHandlingResult HandleException( return result; } + if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + saveError(policyResult, ex, errorContext, token); + if (token.IsCancellationRequested) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + } + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); policyResult.AddBulkProcessorErrors(bulkProcessResult); if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) From ea5723f67ed830b308bcb247a730e9a8a710ac95 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 13 Jan 2026 16:06:01 +0300 Subject: [PATCH 024/109] DRY refactor catch block into a private method used by `SimplePolicyProcessor.Execute` and `SimplePolicyProcessor.Execute`. --- src/Simple/SimplePolicyProcessor.cs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index efbabeb5..9ec47f71 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -116,8 +116,6 @@ private PolicyResult Execute(Action action, EmptyErrorContext emptyErrorContext, result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { action(); @@ -133,22 +131,12 @@ private PolicyResult Execute(Action action, EmptyErrorContext emptyErrorContext, } 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; } @@ -592,7 +580,7 @@ private ExceptionHandlingResult HandleException( CancellationToken token) { - return HandleException(ex, policyResult, errorContext, DefaultErrorSaver, DefaultPolicyRule, ExceptionHandlingBehavior.ConditionalRethrow, ErrorProcessingCancellationEffect.Propagate, token); + return HandleException(ex, policyResult, errorContext, DefaultErrorSaver, DefaultPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, token); } } } From 0e2514bc8b73425c5a66f30a6ffd4bc317da4921 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 14 Jan 2026 16:20:15 +0300 Subject: [PATCH 025/109] DRY refactor catch block into a private method used by `SimplePolicyProcessor.Execute` and `SimplePolicyProcessor. Execute`. --- src/Simple/SimplePolicyProcessor.cs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 9ec47f71..3412f53b 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -226,8 +226,6 @@ private PolicyResult Execute(Func func, EmptyErrorContext emptyErrorCon result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(); @@ -244,22 +242,12 @@ private PolicyResult Execute(Func func, EmptyErrorContext emptyErrorCon } 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; } From 52127cb36307939842f99ee4ddd8758bf67c7cdc Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 15 Jan 2026 17:42:08 +0300 Subject: [PATCH 026/109] Added the internal static `PolicyProcessor.CreateDefaultAsyncErrorSaver` method. --- src/PolicyProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 8a0e4fec..c772bab8 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 { @@ -123,6 +124,9 @@ protected internal ExceptionHandlingResult HandleException( internal static Action, CancellationToken> CreateDefaultErrorSaver() => (pr, e, _, __) => pr.AddError(e); + internal static Func, bool, CancellationToken, Task> CreateDefaultAsyncErrorSaver() => + (pr, e, _, __, ___) => { pr.AddError(e); return Task.CompletedTask; }; + internal static Func, bool> DefaultPolicyRule { get; } = CreateDefaultPolicyRule(); internal static Func, bool> CreateDefaultPolicyRule() => From 1c237ae70fbd172aab6abec377430f3bc3ee486a Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 16 Jan 2026 11:38:58 +0300 Subject: [PATCH 027/109] Add the internal static `PolicyProcessor.DefaultAsyncErrorSaver` property. --- src/PolicyProcessor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index c772bab8..b2d7d126 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -124,6 +124,8 @@ protected internal ExceptionHandlingResult HandleException( 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; }; From e471cbd3dd7277f267dd3a916aa0fe45365d5305 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 17 Jan 2026 19:30:02 +0300 Subject: [PATCH 028/109] Refactor `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 50 ++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index b2d7d126..0ba8b8f8 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -77,16 +77,16 @@ protected internal ExceptionHandlingResult HandleException( Exception ex, PolicyResult policyResult, ErrorContext errorContext, - Action, CancellationToken> saveError, + Action, CancellationToken> errorSaver, Func, bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, ErrorProcessingCancellationEffect cancellationEffect, CancellationToken token) { - saveError = saveError ?? CreateDefaultErrorSaver(); + var saver = errorSaver ?? CreateDefaultErrorSaver(); if (handlingBehavior != ExceptionHandlingBehavior.ConditionalRethrow) { - saveError(policyResult, ex, errorContext, token); + saver(policyResult, ex, errorContext, token); if (token.IsCancellationRequested) { policyResult.SetFailedAndCanceled(); @@ -94,7 +94,15 @@ protected internal ExceptionHandlingResult HandleException( } } - var result = DetermineExceptionHandlingResult(ex, policyResult, errorContext, saveError, policyRuleFunc, handlingBehavior, token); + var (handleResult, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); + + var result = DetermineExceptionHandlingResult(ex, policyResult, handlingBehavior, handleResult, error); + + if (!(error is null) && handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) + { + saver(policyResult, ex, errorContext, token); + } + if (result != ExceptionHandlingResult.Accepted) { return result; @@ -102,7 +110,7 @@ protected internal ExceptionHandlingResult HandleException( if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) { - saveError(policyResult, ex, errorContext, token); + saver(policyResult, ex, errorContext, token); if (token.IsCancellationRequested) { policyResult.SetFailedAndCanceled(); @@ -134,34 +142,28 @@ internal static Func, bool, Cancellatio internal static Func, bool> CreateDefaultPolicyRule() => (_) => true; - private ExceptionHandlingResult DetermineExceptionHandlingResult( + private ExceptionHandlingResult DetermineExceptionHandlingResult( Exception ex, PolicyResult policyResult, - ErrorContext errorContext, - Action, CancellationToken> saveError, - Func, bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, - CancellationToken token) + HandleCatchBlockResult shouldHandleResult, + Exception error) { - var (result, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); - if (result == HandleCatchBlockResult.FailedByErrorFilter) + if (shouldHandleResult == HandleCatchBlockResult.FailedByErrorFilter) { - if (!(error is null)) - { - if (handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) - { - saveError(policyResult, ex, errorContext, token); - } - policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.ErrorFilter, true)); - policyResult.SetFailedAndFilterUnsatisfied(); - return ExceptionHandlingResult.Handled; - } - 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: @@ -170,7 +172,7 @@ private ExceptionHandlingResult DetermineExceptionHandlingResult( return ExceptionHandlingResult.Handled; } } - if (result == HandleCatchBlockResult.FailedByPolicyRules) + if (shouldHandleResult == HandleCatchBlockResult.FailedByPolicyRules) { policyResult.SetFailedInner(); return ExceptionHandlingResult.Handled; From 55d08d91bc3dfb737f7c1896aa3268543a478dd6 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 18 Jan 2026 14:50:01 +0300 Subject: [PATCH 029/109] Follow up on refactor `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 65 +++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 0ba8b8f8..22e4017b 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -94,9 +94,7 @@ protected internal ExceptionHandlingResult HandleException( } } - var (handleResult, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); - - var result = DetermineExceptionHandlingResult(ex, policyResult, handlingBehavior, handleResult, error); + var (result, error) = DetermineExceptionHandlingResult(ex, policyResult, handlingBehavior, errorContext, policyRuleFunc); if (!(error is null) && handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) { @@ -142,42 +140,49 @@ internal static Func, bool, Cancellatio internal static Func, bool> CreateDefaultPolicyRule() => (_) => true; - private ExceptionHandlingResult DetermineExceptionHandlingResult( + private (ExceptionHandlingResult, Exception) DetermineExceptionHandlingResult( Exception ex, PolicyResult policyResult, ExceptionHandlingBehavior handlingBehavior, - HandleCatchBlockResult shouldHandleResult, - Exception error) + ErrorContext errorContext, + Func, bool> policyRuleFunc = null) { - if (shouldHandleResult == HandleCatchBlockResult.FailedByErrorFilter) + var (handleResult, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); + + return (FailPolicyResultIfRequired(), error); + + ExceptionHandlingResult FailPolicyResultIfRequired() { - switch (handlingBehavior) + if (handleResult == HandleCatchBlockResult.FailedByErrorFilter) { - 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)) - { + switch (handlingBehavior) + { + case ExceptionHandlingBehavior.ConditionalRethrow when !(error is null): policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.ErrorFilter, true)); - } - policyResult.SetFailedAndFilterUnsatisfied(); - return ExceptionHandlingResult.Handled; - default: - policyResult.AddCatchBlockError(new CatchBlockException(new ArgumentOutOfRangeException(nameof(handlingBehavior), handlingBehavior, null), ex, CatchBlockExceptionSource.ErrorFilter, true)); - policyResult.SetFailedAndFilterUnsatisfied(); - return ExceptionHandlingResult.Handled; + 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; + } } + if (handleResult == HandleCatchBlockResult.FailedByPolicyRules) + { + policyResult.SetFailedInner(); + return ExceptionHandlingResult.Handled; + } + return ExceptionHandlingResult.Accepted; } - if (shouldHandleResult == HandleCatchBlockResult.FailedByPolicyRules) - { - policyResult.SetFailedInner(); - return ExceptionHandlingResult.Handled; - } - return ExceptionHandlingResult.Accepted; } private (HandleCatchBlockResult, Exception) ShouldHandleException(Exception ex, ErrorContext errorContext, Func, bool> policyRuleFunc = null) From dcdc9597bf4f436c607938b63081238016548611 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 18 Jan 2026 20:57:18 +0300 Subject: [PATCH 030/109] Added the protected internal `PolicyProcessor.HandleExceptionAsync` method. --- src/PolicyProcessor.cs | 53 +++ tests/PoliNorError.Tests.csproj | 1 + .../PolicyProcessor.HandleException.Tests.cs | 26 +- ...icyProcessor.HandleExceptionAsync.Tests.cs | 405 ++++++++++++++++++ 4 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 tests/PolicyProcessor.HandleExceptionAsync.Tests.cs diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 22e4017b..035f84f5 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -73,6 +73,59 @@ internal PolicyProcessorCatchBlockAsyncHandler GetCatchBlockAsyncHandler(P policyRuleFunc); } + protected internal async Task HandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Func, bool, CancellationToken, Task> errorSaver, + Func, bool> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + 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) = DetermineExceptionHandlingResult(ex, policyResult, handlingBehavior, errorContext, policyRuleFunc); + + 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; + } + } + + 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; + } + protected internal ExceptionHandlingResult HandleException( Exception ex, PolicyResult policyResult, diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index fbf1e9fb..4dbcf047 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -119,6 +119,7 @@ + diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 7fbee3f1..e5a5469f 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -11,7 +11,7 @@ namespace PoliNorError.Tests { [TestFixture] - public class PolicyProcessorTests + public partial class PolicyProcessorTests { private class TestPolicyProcessor : PolicyProcessor { @@ -32,6 +32,20 @@ public ExceptionHandlingResult TestHandleException( { return HandleException(ex, policyResult, errorContext, null, policyRuleFunc, handlingBehavior, cancellationEffect, token); } + + public Task TestHandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + ErrorContext errorContext, + Func, bool, CancellationToken, Task> errorSaver, + Func, bool> policyRuleFunc, + ExceptionHandlingBehavior handlingBehavior, + ErrorProcessingCancellationEffect cancellationEffect, + bool configureAwait, + CancellationToken token) + { + return HandleExceptionAsync(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, cancellationEffect, configureAwait, token); + } } private class TestErrorContext : ErrorContext @@ -48,6 +62,8 @@ 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(); @@ -59,8 +75,14 @@ public BulkProcessResult Process(Exception handlingError, ProcessingErrorContext return ResultToReturn ?? new BulkProcessResult(handlingError, null); } - public Task ProcessAsync(Exception handlingError, ProcessingErrorContext errorContext = null, bool configAwait = false, CancellationToken token = default) => throw new NotImplementedException(); + 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] diff --git a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs new file mode 100644 index 00000000..d075ed93 --- /dev/null +++ b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs @@ -0,0 +1,405 @@ +using NUnit.Framework; +using System; +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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + 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"); + + bool policyRuleFunc(ErrorContext _) => false; + + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + policyRuleFunc, + ExceptionHandlingBehavior.ConditionalRethrow, + ErrorProcessingCancellationEffect.Ignore, + false, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + CancellationToken.None); + + 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, + 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, + 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"); + + bool policyRuleFunc(ErrorContext _) => true; + + var result = await processor.TestHandleExceptionAsync( + exception, + policyResult, + errorContext, + null, + policyRuleFunc, + ExceptionHandlingBehavior.Handle, + ErrorProcessingCancellationEffect.Ignore, + false, + CancellationToken.None); + + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + } + } +} From bf02cf6272cf56f3e4d1e9ca179876fac52277b7 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 19 Jan 2026 13:30:57 +0300 Subject: [PATCH 031/109] DRY refactor catch block into a private method used by `SimplePolicyProcessor.ExecuteAsync` and `SimplePolicyProcessor.ExecuteAsync`. --- src/Simple/SimplePolicyProcessor.cs | 31 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 3412f53b..38047136 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -342,8 +342,6 @@ private async Task ExecuteAsync(Func func result.SetExecuted(); - var exHandler = new SimpleAsyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), configureAwait, token); - try { await func(token).ConfigureAwait(configureAwait); @@ -355,22 +353,12 @@ private async Task ExecuteAsync(Func func } catch (Exception ex) { - if (_rethrowIfErrorFilterUnsatisfied) + var handlingResult = await HandleExceptionAsync(ex, result, emptyErrorContext, configureAwait, 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; } - - await exHandler.HandleAsync(ex, emptyErrorContext).ConfigureAwait(configureAwait); } return result; } @@ -570,5 +558,16 @@ private ExceptionHandlingResult HandleException( { return HandleException(ex, policyResult, errorContext, DefaultErrorSaver, DefaultPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, token); } + + private Task HandleExceptionAsync( + Exception ex, + PolicyResult policyResult, + EmptyErrorContext errorContext, + bool configAwait, + CancellationToken token) + + { + return HandleExceptionAsync(ex, policyResult, errorContext, DefaultAsyncErrorSaver, DefaultPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, configAwait, token); + } } } From 5a2aa437a05a4a15d0b3415f777e2ae701270606 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 20 Jan 2026 14:58:46 +0300 Subject: [PATCH 032/109] Refactor `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 64 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 035f84f5..605b16c2 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -147,7 +147,7 @@ protected internal ExceptionHandlingResult HandleException( } } - var (result, error) = DetermineExceptionHandlingResult(ex, policyResult, handlingBehavior, errorContext, policyRuleFunc); + var (result, error) = EvaluateExceptionFilter(policyResult, ex, handlingBehavior); if (!(error is null) && handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) { @@ -169,6 +169,12 @@ protected internal ExceptionHandlingResult HandleException( } } + var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + if (ruleResult != ExceptionHandlingResult.Accepted) + { + return ruleResult; + } + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); policyResult.AddBulkProcessorErrors(bulkProcessResult); if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) @@ -263,6 +269,62 @@ ExceptionHandlingResult FailPolicyResultIfRequired() } } + 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 ExceptionHandlingResult EvaluatePolicyRule(PolicyResult policyResult, ErrorContext errorContext, Func, bool> policyRuleFunc) + { + var ruleApplyResult = policyRuleFunc?.Invoke(errorContext); + if (ruleApplyResult == false) + { + policyResult.SetFailedInner(); + return ExceptionHandlingResult.Handled; + } + return ExceptionHandlingResult.Accepted; + } + public IEnumerator GetEnumerator() => _bulkErrorProcessor.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } From 527ad70c49f4a76fa3111f0a04785d415e58d1bb Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 21 Jan 2026 15:33:02 +0300 Subject: [PATCH 033/109] New tests for `PolicyProcessor.HandleException`. --- tests/PipelineResultTests.cs | 4 +- .../PolicyProcessor.HandleException.Tests.cs | 144 +++++++++++++++++- 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/tests/PipelineResultTests.cs b/tests/PipelineResultTests.cs index 1308e01a..cd242d57 100644 --- a/tests/PipelineResultTests.cs +++ b/tests/PipelineResultTests.cs @@ -87,8 +87,8 @@ public void Should_Create_Success_PipelineResult_With_Result_Value() [Test] public void Should_Create_Success_PipelineResult_With_Reference_Type() { - // Arrange - var expectedValue = "test string"; + // Arrange + const string expectedValue = "test string"; var policyResult = new PolicyResult(); policyResult.SetOk(); policyResult.SetResult(expectedValue); diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index e5a5469f..caf504c1 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -15,10 +15,13 @@ public partial class PolicyProcessorTests { private class TestPolicyProcessor : PolicyProcessor { - public TestPolicyProcessor(IBulkErrorProcessor bulkErrorProcessor) + public TestPolicyProcessor(IBulkErrorProcessor bulkErrorProcessor, bool testFilterUnsatisfied = false) { _bulkErrorProcessor = bulkErrorProcessor; - ErrorFilter.AddExcludedErrorFilter(); + if (testFilterUnsatisfied) + { + ErrorFilter.AddExcludedErrorFilter(); + } } public ExceptionHandlingResult TestHandleException( @@ -28,9 +31,10 @@ public ExceptionHandlingResult TestHandleException( CancellationToken token, Func, bool> policyRuleFunc = null, ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle, - ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate) + ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate, + Action, CancellationToken> errorSaver = null) { - return HandleException(ex, policyResult, errorContext, null, policyRuleFunc, handlingBehavior, cancellationEffect, token); + return HandleException(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, cancellationEffect, token); } public Task TestHandleExceptionAsync( @@ -161,7 +165,7 @@ public void Should_ReturnRethrow_WhenErrorFilterFailsAndBehaviorIsConditionalRet { // Arrange var bulkProcessor = new TestBulkErrorProcessor(); - var processor = new TestPolicyProcessor(bulkProcessor); + var processor = new TestPolicyProcessor(bulkProcessor, true); var policyResult = PolicyResult.ForSync(); var errorContext = new TestErrorContext("test"); var exception = new ArgumentException("test exception"); @@ -187,7 +191,7 @@ public void Should_ReturnHandled_WhenErrorFilterFailsAndBehaviorIsHandle() { ResultToReturn = new BulkProcessResult(new ArgumentException("test"), null) }; - var processor = new TestPolicyProcessor(bulkProcessor); + var processor = new TestPolicyProcessor(bulkProcessor, true); var policyResult = PolicyResult.ForSync(); var errorContext = new TestErrorContext("test"); var exception = new ArgumentException("test"); @@ -342,6 +346,134 @@ public void Should_AddExceptionToPolicyResult_WhenHandlingException() 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, + 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, + 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_HandleNullPolicyRuleFunc() + { + // 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, + null); // null policyRuleFunc + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); + } + + [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 _) => true; + + // Act + var result = processor.TestHandleException( + exception, + policyResult, + errorContext, + CancellationToken.None, + policyRuleFunc, + ExceptionHandlingBehavior.ConditionalRethrow); + + // Assert + Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); + Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); } } } From 025ce8f329552d6f2516c823e3c2d352b0f85226 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 22 Jan 2026 12:59:01 +0300 Subject: [PATCH 034/109] Refactor `PolicyProcessor.HandleExceptionAsync`. --- src/PolicyProcessor.cs | 78 ++----------------- .../PolicyProcessor.HandleException.Tests.cs | 4 +- ...icyProcessor.HandleExceptionAsync.Tests.cs | 68 ++++++++++++++++ 3 files changed, 77 insertions(+), 73 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 605b16c2..b7c36ed7 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -95,7 +95,7 @@ protected internal async Task HandleExceptionAsync( } } - var (result, error) = DetermineExceptionHandlingResult(ex, policyResult, handlingBehavior, errorContext, policyRuleFunc); + var (result, error) = EvaluateExceptionFilter(policyResult, ex, handlingBehavior); if (!(error is null) && handlingBehavior == ExceptionHandlingBehavior.ConditionalRethrow) { @@ -117,6 +117,12 @@ protected internal async Task HandleExceptionAsync( } } + var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + 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) @@ -199,76 +205,6 @@ internal static Func, bool, Cancellatio internal static Func, bool> CreateDefaultPolicyRule() => (_) => true; - private (ExceptionHandlingResult, Exception) DetermineExceptionHandlingResult( - Exception ex, - PolicyResult policyResult, - ExceptionHandlingBehavior handlingBehavior, - ErrorContext errorContext, - Func, bool> policyRuleFunc = null) - { - var (handleResult, error) = ShouldHandleException(ex, errorContext, policyRuleFunc); - - return (FailPolicyResultIfRequired(), error); - - ExceptionHandlingResult FailPolicyResultIfRequired() - { - if (handleResult == HandleCatchBlockResult.FailedByErrorFilter) - { - 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; - } - } - if (handleResult == HandleCatchBlockResult.FailedByPolicyRules) - { - policyResult.SetFailedInner(); - return ExceptionHandlingResult.Handled; - } - return ExceptionHandlingResult.Accepted; - } - } - - private (HandleCatchBlockResult, Exception) ShouldHandleException(Exception ex, ErrorContext errorContext, Func, bool> policyRuleFunc = null) - { - var (filterPassed, error) = RunErrorFilterFunc(); - - if (!filterPassed) - return (HandleCatchBlockResult.FailedByErrorFilter, error); - - if (policyRuleFunc?.Invoke(errorContext) == false) - return (HandleCatchBlockResult.FailedByPolicyRules, null); - - return (HandleCatchBlockResult.Success, null); - - (bool, Exception) RunErrorFilterFunc() - { - try - { - return (ErrorFilter.GetCanHandle()(ex), null); - } - catch (Exception exIn) - { - return (false, exIn); - } - } - } - private (ExceptionHandlingResult, Exception) EvaluateExceptionFilter(PolicyResult policyResult, Exception ex, ExceptionHandlingBehavior handlingBehavior) { var (filterPassed, error) = RunErrorFilterFunc(); diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index caf504c1..c2801055 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -395,7 +395,7 @@ public void Should_HandleErrorFilterThrowingException_WhenHandleOrRethrowBehavio var processor = new TestPolicyProcessor(bulkProcessor); // Make the error filter throw an exception - processor.ErrorFilter.AddIncludedErrorFilter((ex) => save(ex)); + processor.ErrorFilter.AddIncludedErrorFilter((ex) => Save(ex)); var policyResult = PolicyResult.ForSync(); var errorContext = new TestErrorContext("test"); @@ -418,7 +418,7 @@ public void Should_HandleErrorFilterThrowingException_WhenHandleOrRethrowBehavio } #pragma warning disable S1172 // Unused method parameters should be removed - private bool save(Exception _) => throw new InvalidOperationException("Filter error"); + private bool Save(Exception _) => throw new InvalidOperationException("Filter error"); #pragma warning restore S1172 // Unused method parameters should be removed [Test] diff --git a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs index d075ed93..21fad95d 100644 --- a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs +++ b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using static PoliNorError.BulkErrorProcessor; @@ -401,5 +402,72 @@ public async Task Should_HandleExceptionAsync_ReturnHandled_WhenPolicyRuleFuncRe 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, + CancellationToken.None); + 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, + CancellationToken.None); + 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, + CancellationToken.None); + 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)); + } } } From 2d3857707a748a441ee1d876a00709005893a6cb Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 23 Jan 2026 14:43:11 +0300 Subject: [PATCH 035/109] Correct usage of `SimplePolicyProcessor.HandleExceptionAsync` with the `configureAwait` parameter. --- src/Simple/SimplePolicyProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 38047136..62755026 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -353,7 +353,7 @@ private async Task ExecuteAsync(Func func } catch (Exception ex) { - var handlingResult = await HandleExceptionAsync(ex, result, emptyErrorContext, configureAwait, token); + var handlingResult = await HandleExceptionAsync(ex, result, emptyErrorContext, configureAwait, token).ConfigureAwait(configureAwait); if (handlingResult == ExceptionHandlingResult.Rethrow) { ex.Data[PolinorErrorConsts.EXCEPTION_DATA_ERRORFILTERUNSATISFIED_KEY] = true; From 3902e66c58a06cb7757af154ad202560d2481545 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 24 Jan 2026 14:53:47 +0300 Subject: [PATCH 036/109] DRY refactor catch block into a private method used by `SimplePolicyProcessor.ExecuteAsync` and `SimplePolicyProcessor. ExecuteAsync`. --- src/Simple/SimplePolicyProcessor.cs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 62755026..b5b361e8 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -454,8 +454,6 @@ private async Task> ExecuteAsync(Func> ExecuteAsync(Func Date: Sun, 25 Jan 2026 19:00:18 +0300 Subject: [PATCH 037/109] DRY refactor catch-block handling in `SimplePolicyProcessor.ExecuteAsync`. --- src/Simple/SimplePolicyProcessor.cs | 20 ++++---------------- tests/ErrorWithInnerExcThrowingFuncs.cs | 2 ++ tests/SimplePolicyProcessorTests.cs | 8 ++++++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index b5b361e8..0bad2d03 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -405,8 +405,6 @@ private async Task> ExecuteAsync(Func> ExecuteAsync(Func 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 class TestExceptionWithInnerException : Exception diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 07fdd29a..44511d36 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -836,6 +836,14 @@ public void Should_Rethrow_With_ThrowIfErrorFilterUnsatisfied_True_ForExecuteWit 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] [TestCase(true)] [TestCase(false)] From f4e47db4056e38ebc22f599ed1b20b865a84a1c1 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 26 Jan 2026 16:10:57 +0300 Subject: [PATCH 038/109] DRY refactor catch-block handling in `SimplePolicyProcessor.Execute`. --- src/Simple/SimplePolicyProcessor.cs | 20 ++++---------------- tests/ErrorWithInnerExcThrowingFuncs.cs | 2 ++ tests/SimplePolicyProcessorTests.cs | 8 ++++++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 0bad2d03..1c53283c 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -173,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); @@ -191,22 +189,12 @@ private PolicyResult Execute(Func func, TParam param, E } 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; } diff --git a/tests/ErrorWithInnerExcThrowingFuncs.cs b/tests/ErrorWithInnerExcThrowingFuncs.cs index abe17db3..15461e14 100644 --- a/tests/ErrorWithInnerExcThrowingFuncs.cs +++ b/tests/ErrorWithInnerExcThrowingFuncs.cs @@ -27,6 +27,8 @@ public static class ErrorWithInnerExcThrowingFuncs 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/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 44511d36..c0d0a7c3 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -844,6 +844,14 @@ public void Should_Rethrow_With_ThrowIfErrorFilterUnsatisfied_True_ForExecuteAsy 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)] From 6568e5d1479d313ea54f803a4d6bd548755932cf Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 27 Jan 2026 17:33:10 +0300 Subject: [PATCH 039/109] Introduce the `ICanAddErrorFilter.AddExceptionFilter` extension method. --- src/CatchBlockHandlers/CatchBlockFilter.cs | 2 +- src/ICanAddErrorFilter.cs | 19 +++++++++++++++++++ tests/ICanAddErrorFilterTests.cs | 12 ++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) 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/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/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); + } } } From 0dc3e30e2202aee82445f62632281eb208dff5cd Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 28 Jan 2026 17:27:55 +0300 Subject: [PATCH 040/109] Introduce the `ExceptionFilter.IncludeErrorSet` extension method. --- .../PolicyProcessor.ExceptionFilter.cs | 6 ++++++ tests/ExceptionFilterTests.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs index d3c16a1e..290f0a9c 100644 --- a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs +++ b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs @@ -12,6 +12,12 @@ public class ExceptionFilter public IEnumerable>> ExcludedErrorFilters => FilterSet.ExcludedErrorFilters; + public ExceptionFilter IncludeErrorSet(IErrorSet errorSet) + { + this.AddIncludedErrorSet(errorSet); + return this; + } + internal ExceptionFilterSet FilterSet { get; } = new ExceptionFilterSet(); internal void AddIncludedErrorFilter(Expression> handledErrorFilter) diff --git a/tests/ExceptionFilterTests.cs b/tests/ExceptionFilterTests.cs index ed4d3e0f..b5af9ccb 100644 --- a/tests/ExceptionFilterTests.cs +++ b/tests/ExceptionFilterTests.cs @@ -450,6 +450,19 @@ 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); + } } [TestFixture] From 9617ce60a5cf57df142934646f8cb2f8792aec16 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 29 Jan 2026 12:02:31 +0300 Subject: [PATCH 041/109] Deprecate internal `FallbackFuncExecResult.FromErrorAndToken` overloads. --- src/Fallback/FallbackFuncExecResult.cs | 6 ++++++ 1 file changed, 6 insertions(+) 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)) From 4d33d59ee9793ea03e270203630b2cca9c29445a Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 29 Jan 2026 14:00:05 +0300 Subject: [PATCH 042/109] Introduce `ProcessingOrder` enum. --- src/PolicyProcessor.cs | 41 +++++++++++++++---- src/Simple/SimplePolicyProcessor.cs | 11 ++++- .../PolicyProcessor.HandleException.Tests.cs | 2 +- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index b7c36ed7..c294374c 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -139,6 +139,7 @@ protected internal ExceptionHandlingResult HandleException( Action, CancellationToken> errorSaver, Func, bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, + ProcessingOrder processingOrder, ErrorProcessingCancellationEffect cancellationEffect, CancellationToken token) { @@ -175,18 +176,26 @@ protected internal ExceptionHandlingResult HandleException( } } - var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); - if (ruleResult != ExceptionHandlingResult.Accepted) + if (processingOrder == ProcessingOrder.EvaluateThenProcess) { - return ruleResult; - } + var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + if (ruleResult != ExceptionHandlingResult.Accepted) + { + return ruleResult; + } - var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); - policyResult.AddBulkProcessorErrors(bulkProcessResult); - if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); + policyResult.AddBulkProcessorErrors(bulkProcessResult); + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + } + } + else { - policyResult.SetFailedAndCanceled(); + throw new NotImplementedException(); } + return ExceptionHandlingResult.Handled; } @@ -333,4 +342,20 @@ public enum ErrorProcessingCancellationEffect /// 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/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 1c53283c..e304b352 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -520,7 +520,16 @@ private ExceptionHandlingResult HandleException( CancellationToken token) { - return HandleException(ex, policyResult, errorContext, DefaultErrorSaver, DefaultPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, token); + return HandleException( + ex, + policyResult, + errorContext, + DefaultErrorSaver, + DefaultPolicyRule, + _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, + ProcessingOrder.EvaluateThenProcess, + ErrorProcessingCancellationEffect.Propagate, + token); } private Task HandleExceptionAsync( diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index c2801055..3f8e800d 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -34,7 +34,7 @@ public ExceptionHandlingResult TestHandleException( ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate, Action, CancellationToken> errorSaver = null) { - return HandleException(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, cancellationEffect, token); + return HandleException(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, ProcessingOrder.EvaluateThenProcess, cancellationEffect, token); } public Task TestHandleExceptionAsync( From c3b5ee251ac65b2a75fba2cc6f8f4d1d080a36f3 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 30 Jan 2026 17:34:02 +0300 Subject: [PATCH 043/109] DRY refactor catch-block handling in `SimplePolicyProcessor.ExecuteAsync`. --- src/Simple/SimplePolicyProcessor.cs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index e304b352..8df7b2e1 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -282,8 +282,6 @@ private async Task ExecuteAsync(Func ExecuteAsync(Func Date: Sat, 31 Jan 2026 17:56:24 +0300 Subject: [PATCH 044/109] Implement `ProcessingOrder.ProcessThenEvaluate` path in `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 15 ++++- .../PolicyProcessor.HandleException.Tests.cs | 67 ++++++++++--------- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index c294374c..7b35835c 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -190,13 +190,22 @@ protected internal ExceptionHandlingResult HandleException( { policyResult.SetFailedAndCanceled(); } + + return ExceptionHandlingResult.Handled; } else { - throw new NotImplementedException(); - } + var bulkProcessResult = _bulkErrorProcessor.Process(ex, errorContext.ToProcessingErrorContext(), token); + policyResult.AddBulkProcessorErrors(bulkProcessResult); - return ExceptionHandlingResult.Handled; + if (cancellationEffect == ErrorProcessingCancellationEffect.Propagate && bulkProcessResult.IsCanceled) + { + policyResult.SetFailedAndCanceled(); + return ExceptionHandlingResult.Handled; + } + + return EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + } } internal static Action, CancellationToken> DefaultErrorSaver { get; } = CreateDefaultErrorSaver(); diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 3f8e800d..19983c66 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -29,12 +29,13 @@ public ExceptionHandlingResult TestHandleException( PolicyResult policyResult, ErrorContext errorContext, CancellationToken token, + ProcessingOrder processingOrder = ProcessingOrder.EvaluateThenProcess, Func, bool> policyRuleFunc = null, ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate, Action, CancellationToken> errorSaver = null) { - return HandleException(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, ProcessingOrder.EvaluateThenProcess, cancellationEffect, token); + return HandleException(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, processingOrder, cancellationEffect, token); } public Task TestHandleExceptionAsync( @@ -128,7 +129,7 @@ public void Should_Set_PolicyResult_IsCanceled_DependOn_CancellationEffect(Error var processor = new TestPolicyProcessor(bulkProcessor); - var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token, null, ExceptionHandlingBehavior.Handle, cancellationEffect); + var result = processor.TestHandleException(exception, policyResult, errorContext, cts.Token, ProcessingOrder.EvaluateThenProcess, null, ExceptionHandlingBehavior.Handle, cancellationEffect); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); @@ -140,7 +141,32 @@ public void Should_Set_PolicyResult_IsCanceled_DependOn_CancellationEffect(Error } [Test] - public void Should_ReturnHandled_WhenExceptionIsInvalidOperationException() + 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 @@ -176,6 +202,7 @@ public void Should_ReturnRethrow_WhenErrorFilterFailsAndBehaviorIsConditionalRet policyResult, errorContext, CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, null, ExceptionHandlingBehavior.ConditionalRethrow); @@ -202,6 +229,7 @@ public void Should_ReturnHandled_WhenErrorFilterFailsAndBehaviorIsHandle() policyResult, errorContext, CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, null, ExceptionHandlingBehavior.Handle); @@ -228,6 +256,7 @@ public void Should_ReturnHandled_AndSetFailed_WhenPolicyRuleFuncReturnsFalse() policyResult, errorContext, CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, policyRuleFunc); // Assert @@ -256,6 +285,7 @@ public void Should_ReturnHandled_WhenPolicyRuleFuncReturnsTrue() policyResult, errorContext, CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, policyRuleFunc); // Assert @@ -374,6 +404,7 @@ void customErrorSaver(PolicyResult pr, Exception _, ErrorContext __, Can policyResult, errorContext, CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, @@ -407,7 +438,8 @@ public void Should_HandleErrorFilterThrowingException_WhenHandleOrRethrowBehavio policyResult, errorContext, CancellationToken.None, - null, + ProcessingOrder.EvaluateThenProcess, + null, handlingBehavior ); // Assert @@ -421,32 +453,6 @@ public void Should_HandleErrorFilterThrowingException_WhenHandleOrRethrowBehavio private bool Save(Exception _) => throw new InvalidOperationException("Filter error"); #pragma warning restore S1172 // Unused method parameters should be removed - [Test] - public void Should_HandleNullPolicyRuleFunc() - { - // 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, - null); // null policyRuleFunc - - // Assert - Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); - Assert.That(policyResult.Errors.Count, Is.EqualTo(1)); - } - [Test] public void Should_ReturnHandled_WhenConditionalRethrowWithValidFilterAndPolicyRule() { @@ -468,6 +474,7 @@ public void Should_ReturnHandled_WhenConditionalRethrowWithValidFilterAndPolicyR policyResult, errorContext, CancellationToken.None, + ProcessingOrder.EvaluateThenProcess, policyRuleFunc, ExceptionHandlingBehavior.ConditionalRethrow); From c19ab01f2060152ee1e376c994b8717f3f1f2872 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 1 Feb 2026 20:39:14 +0300 Subject: [PATCH 045/109] Add `CancellationToken` parameter to policy rule `Func` when handling exceptions in `SimplePolicyProcessor` catch blocks. --- src/PolicyProcessor.cs | 20 +++++++++---------- .../PolicyProcessor.HandleException.Tests.cs | 10 +++++----- ...icyProcessor.HandleExceptionAsync.Tests.cs | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 7b35835c..95ef6897 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -78,7 +78,7 @@ protected internal async Task HandleExceptionAsync( PolicyResult policyResult, ErrorContext errorContext, Func, bool, CancellationToken, Task> errorSaver, - Func, bool> policyRuleFunc, + Func, CancellationToken, bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, ErrorProcessingCancellationEffect cancellationEffect, bool configureAwait, @@ -117,7 +117,7 @@ protected internal async Task HandleExceptionAsync( } } - var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc, token); if (ruleResult != ExceptionHandlingResult.Accepted) { return ruleResult; @@ -137,7 +137,7 @@ protected internal ExceptionHandlingResult HandleException( PolicyResult policyResult, ErrorContext errorContext, Action, CancellationToken> errorSaver, - Func, bool> policyRuleFunc, + Func, CancellationToken, bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, ProcessingOrder processingOrder, ErrorProcessingCancellationEffect cancellationEffect, @@ -178,7 +178,7 @@ protected internal ExceptionHandlingResult HandleException( if (processingOrder == ProcessingOrder.EvaluateThenProcess) { - var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc, token); if (ruleResult != ExceptionHandlingResult.Accepted) { return ruleResult; @@ -204,7 +204,7 @@ protected internal ExceptionHandlingResult HandleException( return ExceptionHandlingResult.Handled; } - return EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc); + return EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc, token); } } @@ -218,10 +218,10 @@ internal static Action, CancellationTok internal static Func, bool, CancellationToken, Task> CreateDefaultAsyncErrorSaver() => (pr, e, _, __, ___) => { pr.AddError(e); return Task.CompletedTask; }; - internal static Func, bool> DefaultPolicyRule { get; } = CreateDefaultPolicyRule(); + internal static Func, CancellationToken, bool> DefaultPolicyRule { get; } = CreateDefaultPolicyRule(); - internal static Func, bool> CreateDefaultPolicyRule() => - (_) => true; + internal static Func, CancellationToken, bool> CreateDefaultPolicyRule() => + (_, __) => true; private (ExceptionHandlingResult, Exception) EvaluateExceptionFilter(PolicyResult policyResult, Exception ex, ExceptionHandlingBehavior handlingBehavior) { @@ -268,9 +268,9 @@ ExceptionHandlingResult FailPolicyResultIfRequired() } } - private ExceptionHandlingResult EvaluatePolicyRule(PolicyResult policyResult, ErrorContext errorContext, Func, bool> policyRuleFunc) + private ExceptionHandlingResult EvaluatePolicyRule(PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, bool> policyRuleFunc, CancellationToken token) { - var ruleApplyResult = policyRuleFunc?.Invoke(errorContext); + var ruleApplyResult = policyRuleFunc?.Invoke(errorContext, token); if (ruleApplyResult == false) { policyResult.SetFailedInner(); diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 19983c66..4faac9f2 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -30,7 +30,7 @@ public ExceptionHandlingResult TestHandleException( ErrorContext errorContext, CancellationToken token, ProcessingOrder processingOrder = ProcessingOrder.EvaluateThenProcess, - Func, bool> policyRuleFunc = null, + Func, CancellationToken, bool> policyRuleFunc = null, ExceptionHandlingBehavior handlingBehavior = ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect cancellationEffect = ErrorProcessingCancellationEffect.Propagate, Action, CancellationToken> errorSaver = null) @@ -43,7 +43,7 @@ public Task TestHandleExceptionAsync( PolicyResult policyResult, ErrorContext errorContext, Func, bool, CancellationToken, Task> errorSaver, - Func, bool> policyRuleFunc, + Func, CancellationToken,bool> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, ErrorProcessingCancellationEffect cancellationEffect, bool configureAwait, @@ -248,7 +248,7 @@ public void Should_ReturnHandled_AndSetFailed_WhenPolicyRuleFuncReturnsFalse() var policyResult = PolicyResult.ForSync(); var errorContext = new TestErrorContext("test"); var exception = new InvalidOperationException("test exception"); - bool policyRuleFunc(ErrorContext _) => false; + bool policyRuleFunc(ErrorContext _, CancellationToken __) => false; // Act var result = processor.TestHandleException( @@ -277,7 +277,7 @@ public void Should_ReturnHandled_WhenPolicyRuleFuncReturnsTrue() var policyResult = PolicyResult.ForSync(); var errorContext = new TestErrorContext("test"); var exception = new InvalidOperationException("test exception"); - bool policyRuleFunc(ErrorContext _) => true; + bool policyRuleFunc(ErrorContext _, CancellationToken __) => true; // Act var result = processor.TestHandleException( @@ -466,7 +466,7 @@ public void Should_ReturnHandled_WhenConditionalRethrowWithValidFilterAndPolicyR var errorContext = new TestErrorContext("test"); var exception = new InvalidOperationException("test exception"); - bool policyRuleFunc(ErrorContext _) => true; + bool policyRuleFunc(ErrorContext _, CancellationToken __) => true; // Act var result = processor.TestHandleException( diff --git a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs index 21fad95d..4d081b06 100644 --- a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs +++ b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs @@ -122,7 +122,7 @@ public async Task Should_ReturnRethrow_WhenPolicyRuleFuncReturnsFalseAndBehavior var exception = new Exception("Test exception"); var errorContext = new TestErrorContext("test"); - bool policyRuleFunc(ErrorContext _) => false; + bool policyRuleFunc(ErrorContext _, CancellationToken __) => false; var result = await processor.TestHandleExceptionAsync( exception, @@ -387,7 +387,7 @@ public async Task Should_HandleExceptionAsync_ReturnHandled_WhenPolicyRuleFuncRe var exception = new Exception("Test exception"); var errorContext = new TestErrorContext("test"); - bool policyRuleFunc(ErrorContext _) => true; + bool policyRuleFunc(ErrorContext _, CancellationToken __) => true; var result = await processor.TestHandleExceptionAsync( exception, From bdb66522b96fee8ce04559143e4d62dbe79d6f23 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 2 Feb 2026 16:23:09 +0300 Subject: [PATCH 046/109] Refactor private `EvaluatePolicyRule` method called from `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 56 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 95ef6897..d5a22313 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -117,7 +117,7 @@ protected internal async Task HandleExceptionAsync( } } - var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc, token); + var ruleResult = EvaluatePolicyRule(ex, policyResult, errorContext, policyRuleFunc, token); if (ruleResult != ExceptionHandlingResult.Accepted) { return ruleResult; @@ -178,7 +178,7 @@ protected internal ExceptionHandlingResult HandleException( if (processingOrder == ProcessingOrder.EvaluateThenProcess) { - var ruleResult = EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc, token); + var ruleResult = EvaluatePolicyRule(ex, policyResult, errorContext, policyRuleFunc, token); if (ruleResult != ExceptionHandlingResult.Accepted) { return ruleResult; @@ -204,7 +204,7 @@ protected internal ExceptionHandlingResult HandleException( return ExceptionHandlingResult.Handled; } - return EvaluatePolicyRule(policyResult, errorContext, policyRuleFunc, token); + return EvaluatePolicyRule(ex, policyResult, errorContext, policyRuleFunc, token); } } @@ -268,15 +268,55 @@ ExceptionHandlingResult FailPolicyResultIfRequired() } } - private ExceptionHandlingResult EvaluatePolicyRule(PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, bool> policyRuleFunc, CancellationToken token) + private static ExceptionHandlingResult EvaluatePolicyRule(Exception ex, PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, bool> policyRuleFunc, CancellationToken token) { - var ruleApplyResult = policyRuleFunc?.Invoke(errorContext, token); - if (ruleApplyResult == false) + var(accepted, canceled, error) = RunPolicyRuleFunc(); + if (accepted) { - policyResult.SetFailedInner(); + return ExceptionHandlingResult.Accepted; + } + else + { + if (!(error is null)) + { + if (canceled) + { + policyResult.SetFailedAndCanceled(); + policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.PolicyRule)); + } + else + { + policyResult.SetFailedWithCatchBlockError(error, ex, CatchBlockExceptionSource.PolicyRule); + } + } + else + { + policyResult.SetFailedInner(); + } + return ExceptionHandlingResult.Handled; } - return ExceptionHandlingResult.Accepted; + + (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); + } + } } public IEnumerator GetEnumerator() => _bulkErrorProcessor.GetEnumerator(); From c289ffc6f8db27b7bf1b9bbc2376535452521a66 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 3 Feb 2026 15:07:36 +0300 Subject: [PATCH 047/109] Simplify cancellation-related `AggregateException` filtering in synchronous policy processor methods to a single `CancellationToken.IsCancellationRequested` check. --- src/Utilities/ExceptionExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Utilities/ExceptionExtensions.cs b/src/Utilities/ExceptionExtensions.cs index 4370ef11..1e0a7f1d 100644 --- a/src/Utilities/ExceptionExtensions.cs +++ b/src/Utilities/ExceptionExtensions.cs @@ -6,8 +6,10 @@ 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); +#pragma warning disable RCS1175 // Unused this parameter. + public static bool IsOperationCanceledWithRequestedToken(this AggregateException ae, +#pragma warning restore RCS1175 // Unused this parameter. + 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)); From d416f8e69cd23bc482db90378ed4364253c45c9a Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 3 Feb 2026 15:53:42 +0300 Subject: [PATCH 048/109] Add private `EvaluatePolicyRuleAsync` method called from `PolicyProcessor.HandleExceptionAsync`. --- src/PolicyProcessor.cs | 59 ++++++++++++++++++- src/Simple/SimplePolicyProcessor.cs | 2 +- .../PolicyProcessor.HandleException.Tests.cs | 2 +- ...icyProcessor.HandleExceptionAsync.Tests.cs | 4 +- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index d5a22313..05c1ee7c 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -78,7 +78,7 @@ protected internal async Task HandleExceptionAsync( PolicyResult policyResult, ErrorContext errorContext, Func, bool, CancellationToken, Task> errorSaver, - Func, CancellationToken, bool> policyRuleFunc, + Func, CancellationToken, Task> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, ErrorProcessingCancellationEffect cancellationEffect, bool configureAwait, @@ -117,7 +117,7 @@ protected internal async Task HandleExceptionAsync( } } - var ruleResult = EvaluatePolicyRule(ex, policyResult, errorContext, policyRuleFunc, token); + var ruleResult = await EvaluatePolicyRuleAsync(ex, policyResult, errorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); if (ruleResult != ExceptionHandlingResult.Accepted) { return ruleResult; @@ -218,6 +218,8 @@ internal static Action, CancellationTok 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() => @@ -319,6 +321,59 @@ private static ExceptionHandlingResult EvaluatePolicyRule(Exception ex, Polic } } + private static async Task EvaluatePolicyRuleAsync(Exception ex, PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, Task> policyRuleFunc, bool configureAwait, CancellationToken token) + { + var (accepted, canceled, error) = await RunPolicyRuleFunc().ConfigureAwait(configureAwait); + if (accepted) + { + return ExceptionHandlingResult.Accepted; + } + else + { + if (!(error is null)) + { + if (canceled) + { + policyResult.SetFailedAndCanceled(); + policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.PolicyRule)); + } + else + { + policyResult.SetFailedWithCatchBlockError(error, ex, CatchBlockExceptionSource.PolicyRule); + } + } + else + { + policyResult.SetFailedInner(); + } + + return ExceptionHandlingResult.Handled; + } + + 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); + } + } + } + public IEnumerator GetEnumerator() => _bulkErrorProcessor.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 8df7b2e1..1a697b3a 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -528,7 +528,7 @@ private Task HandleExceptionAsync( CancellationToken token) { - return HandleExceptionAsync(ex, policyResult, errorContext, DefaultAsyncErrorSaver, DefaultPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, configAwait, token); + return HandleExceptionAsync(ex, policyResult, errorContext, DefaultAsyncErrorSaver, DefaultAsyncPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, configAwait, token); } } } diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 4faac9f2..8a19ca8b 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -43,7 +43,7 @@ public Task TestHandleExceptionAsync( PolicyResult policyResult, ErrorContext errorContext, Func, bool, CancellationToken, Task> errorSaver, - Func, CancellationToken,bool> policyRuleFunc, + Func, CancellationToken, Task> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, ErrorProcessingCancellationEffect cancellationEffect, bool configureAwait, diff --git a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs index 4d081b06..b7a5afff 100644 --- a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs +++ b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs @@ -122,7 +122,7 @@ public async Task Should_ReturnRethrow_WhenPolicyRuleFuncReturnsFalseAndBehavior var exception = new Exception("Test exception"); var errorContext = new TestErrorContext("test"); - bool policyRuleFunc(ErrorContext _, CancellationToken __) => false; + Task policyRuleFunc(ErrorContext _, CancellationToken __) => Task.FromResult(false); var result = await processor.TestHandleExceptionAsync( exception, @@ -387,7 +387,7 @@ public async Task Should_HandleExceptionAsync_ReturnHandled_WhenPolicyRuleFuncRe var exception = new Exception("Test exception"); var errorContext = new TestErrorContext("test"); - bool policyRuleFunc(ErrorContext _, CancellationToken __) => true; + Task policyRuleFunc(ErrorContext _, CancellationToken __) => Task.FromResult(true); var result = await processor.TestHandleExceptionAsync( exception, From b0b010884b00669875e0d3f7fc62854d34ee75bf Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 4 Feb 2026 17:22:40 +0300 Subject: [PATCH 049/109] Introduce `ServiceOperationCanceledException` for cancellation observed through a linked token when synchronously waiting on tasks. --- .../ServiceOperationCanceledException.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/Exceptions/ServiceOperationCanceledException.cs 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) { } + } +} From c51731c27e1aaccc937471d6463a01d48b475eb8 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 5 Feb 2026 13:37:45 +0300 Subject: [PATCH 050/109] Use `ServiceOperationCanceledException` as the fallback exception in `AggregateException.GetCancellationException`. --- src/Utilities/ExceptionExtensions.cs | 10 +++++----- tests/ExceptionExtensionsTests.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Utilities/ExceptionExtensions.cs b/src/Utilities/ExceptionExtensions.cs index 1e0a7f1d..ad948bb8 100644 --- a/src/Utilities/ExceptionExtensions.cs +++ b/src/Utilities/ExceptionExtensions.cs @@ -14,14 +14,14 @@ public static bool IsOperationCanceledWithRequestedToken(this AggregateException 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/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] From 413e7d38bfed5f21cb26d7d63658ba1956e8934f Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 6 Feb 2026 16:26:15 +0300 Subject: [PATCH 051/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `SimplePolicyProcessor.Execute` and `SimplePolicyProcessor.Execute` methods. --- src/Simple/SimplePolicyProcessor.cs | 2 +- src/Utilities/ExceptionExtensions.cs | 4 +- tests/WrappedPolicyTests.cs | 96 ++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 1a697b3a..8854b375 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -127,7 +127,7 @@ 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) { diff --git a/src/Utilities/ExceptionExtensions.cs b/src/Utilities/ExceptionExtensions.cs index ad948bb8..dd0bb080 100644 --- a/src/Utilities/ExceptionExtensions.cs +++ b/src/Utilities/ExceptionExtensions.cs @@ -6,9 +6,7 @@ namespace PoliNorError { internal static class ExceptionExtensions { -#pragma warning disable RCS1175 // Unused this parameter. - public static bool IsOperationCanceledWithRequestedToken(this AggregateException ae, -#pragma warning restore RCS1175 // Unused this parameter. + public static bool IsOperationCanceledWithRequestedToken(this AggregateException _, CancellationToken token) => token.IsCancellationRequested; public static bool HasCanceledException(this AggregateException ae, CancellationToken token) => ae.Flatten().InnerExceptions diff --git a/tests/WrappedPolicyTests.cs b/tests/WrappedPolicyTests.cs index cd30c99f..c5c19321 100644 --- a/tests/WrappedPolicyTests.cs +++ b/tests/WrappedPolicyTests.cs @@ -943,6 +943,102 @@ 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(GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + } + else + { + pr = policy.Handle(GetFuncWithTaskWait(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 static Action GetFuncWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => + { + if (canceledOnLinkedSource) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) + { + var innerToken = cancelTokenSource.Token; + GetTask(innerToken).Wait(); + } + } + else + { + GetTask(sourceThatWillBeCanceled.Token).Wait(); + } + async Task GetTask(CancellationToken token) + { + await Task.Delay(1); + sourceThatWillBeCanceled.Cancel(); + token.ThrowIfCancellationRequested(); + } + }; + } + + private static Action GetFuncWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => + { + using (var ctsOther = new CancellationTokenSource()) + { + var otherTask = Task.Run(async () => + { + await Task.Delay(1); + ctsOther.Cancel(); + ctsOther.Token.ThrowIfCancellationRequested(); + }); + + async Task GetTask() + { + await Task.Delay(1); + if (canceledOnLinkedSource) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ctsOther.Token)) + { + var innerToken = cancelTokenSource.Token; + sourceThatWillBeCanceled.Cancel(); + innerToken.ThrowIfCancellationRequested(); + } + } + else + { + sourceThatWillBeCanceled.Cancel(); + sourceThatWillBeCanceled.Token.ThrowIfCancellationRequested(); + } + } + Task.WaitAll(otherTask, GetTask()); + } + }; + } + private class AlwaysFailedAndCanceledSimplePolicyProcessor : ISimplePolicyProcessor { private readonly bool _setCanceledExcepton; From 7c03f18ea07ca1234add5c85b945053670a86100 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 7 Feb 2026 18:10:39 +0300 Subject: [PATCH 052/109] Introduce the `ExceptionFilter.ExcludeErrorSet` method. --- .../PolicyProcessor.ExceptionFilter.cs | 6 ++++++ tests/ExceptionFilterTests.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs index 290f0a9c..4d9ffddc 100644 --- a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs +++ b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs @@ -18,6 +18,12 @@ public ExceptionFilter IncludeErrorSet(IErrorSet errorSet) return this; } + public ExceptionFilter ExcludeErrorSet(IErrorSet errorSet) + { + this.AddExcludedErrorSet(errorSet); + return this; + } + internal ExceptionFilterSet FilterSet { get; } = new ExceptionFilterSet(); internal void AddIncludedErrorFilter(Expression> handledErrorFilter) diff --git a/tests/ExceptionFilterTests.cs b/tests/ExceptionFilterTests.cs index b5af9ccb..87116c17 100644 --- a/tests/ExceptionFilterTests.cs +++ b/tests/ExceptionFilterTests.cs @@ -462,6 +462,19 @@ public void Should_ExceptionFilter_IncludeErrorSet(bool inner) 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); } } From 1ad22e28ea9169f60c7a392a6daf9d952d1db078 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 8 Feb 2026 19:28:24 +0300 Subject: [PATCH 053/109] Refactor private `EvaluatePolicyRuleAsync` and `EvaluatePolicyRule` methods invoked by `PolicyProcessor.HandleExceptionAsync` and `PolicyProcessor.HandleException`. --- src/PolicyProcessor.cs | 83 ++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 05c1ee7c..7017c1fd 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -272,32 +272,7 @@ ExceptionHandlingResult FailPolicyResultIfRequired() private static ExceptionHandlingResult EvaluatePolicyRule(Exception ex, PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, bool> policyRuleFunc, CancellationToken token) { - var(accepted, canceled, error) = RunPolicyRuleFunc(); - if (accepted) - { - return ExceptionHandlingResult.Accepted; - } - else - { - if (!(error is null)) - { - if (canceled) - { - policyResult.SetFailedAndCanceled(); - policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.PolicyRule)); - } - else - { - policyResult.SetFailedWithCatchBlockError(error, ex, CatchBlockExceptionSource.PolicyRule); - } - } - else - { - policyResult.SetFailedInner(); - } - - return ExceptionHandlingResult.Handled; - } + return HandlePolicyRuleFuncResult(RunPolicyRuleFunc(), ex, policyResult); (bool Result, bool IsCanceled, Exception error) RunPolicyRuleFunc() { @@ -323,32 +298,7 @@ private static ExceptionHandlingResult EvaluatePolicyRule(Exception ex, Polic private static async Task EvaluatePolicyRuleAsync(Exception ex, PolicyResult policyResult, ErrorContext errorContext, Func, CancellationToken, Task> policyRuleFunc, bool configureAwait, CancellationToken token) { - var (accepted, canceled, error) = await RunPolicyRuleFunc().ConfigureAwait(configureAwait); - if (accepted) - { - return ExceptionHandlingResult.Accepted; - } - else - { - if (!(error is null)) - { - if (canceled) - { - policyResult.SetFailedAndCanceled(); - policyResult.AddCatchBlockError(new CatchBlockException(error, ex, CatchBlockExceptionSource.PolicyRule)); - } - else - { - policyResult.SetFailedWithCatchBlockError(error, ex, CatchBlockExceptionSource.PolicyRule); - } - } - else - { - policyResult.SetFailedInner(); - } - - return ExceptionHandlingResult.Handled; - } + return HandlePolicyRuleFuncResult(await RunPolicyRuleFunc().ConfigureAwait(configureAwait), ex, policyResult); async Task<(bool Result, bool IsCanceled, Exception error)> RunPolicyRuleFunc() { @@ -374,6 +324,35 @@ private static async Task EvaluatePolicyRuleAsync(Ex } } + 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(); } From 4cb304cecafbbea5714db94d11f5c85de8973f4e Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 9 Feb 2026 18:09:20 +0300 Subject: [PATCH 054/109] Add `MethodImpl(MethodImplOptions.AggressiveInlining)` to `PolicyResult.ForSync()` and `ForNotSync()`. --- src/PolicyResult.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PolicyResult.cs b/src/PolicyResult.cs index 651ef708..ac7bb77e 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,12 @@ 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); + internal static PolicyResult InitByConfigureAwait(bool configureAwait) => !configureAwait ? ForNotSync() : ForSync(); /// From 3350bb7afc8d41d4a4f342033f4567c867c0efa0 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Feb 2026 10:54:49 +0300 Subject: [PATCH 055/109] Add tests for `SimplePolicyProcessor.Execute` to verify cancellation handling and that `ServiceOperationCanceledException` is stored in `PolicyResult` on one path. --- tests/PoliNorError.Tests.csproj | 1 + tests/SimplePolicyProcessorTests.cs | 34 ++++++++++++++ tests/TaskWaitingDelegates.cs | 70 +++++++++++++++++++++++++++++ tests/WrappedPolicyTests.cs | 65 +-------------------------- 4 files changed, 107 insertions(+), 63 deletions(-) create mode 100644 tests/TaskWaitingDelegates.cs diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 4dbcf047..1b818981 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -129,6 +129,7 @@ + diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index c0d0a7c3..0201424e 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -1289,6 +1289,40 @@ void action(Exception _, ProcessingErrorInfo pi) Assert.That(result.IsSuccess, Is.True); } + [Test] + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void Should_Execute_Action_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()); + } + } + } + private class TestErrorProcessor : IErrorProcessor { public Exception Process(Exception error, ProcessingErrorInfo catchBlockProcessErrorInfo = null, CancellationToken cancellationToken = default) diff --git a/tests/TaskWaitingDelegates.cs b/tests/TaskWaitingDelegates.cs new file mode 100644 index 00000000..e01fc040 --- /dev/null +++ b/tests/TaskWaitingDelegates.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoliNorError.Tests +{ + internal static class TaskWaitingDelegates + { + public static Action GetFuncWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => + { + if (canceledOnLinkedSource) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) + { + var innerToken = cancelTokenSource.Token; + GetTask(innerToken).Wait(); + } + } + else + { + GetTask(sourceThatWillBeCanceled.Token).Wait(); + } + async Task GetTask(CancellationToken token) + { + await Task.Delay(1); + sourceThatWillBeCanceled.Cancel(); + token.ThrowIfCancellationRequested(); + } + }; + } + + public static Action GetFuncWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + { + return () => + { + using (var ctsOther = new CancellationTokenSource()) + { + var otherTask = Task.Run(async () => + { + await Task.Delay(1); + ctsOther.Cancel(); + ctsOther.Token.ThrowIfCancellationRequested(); + }); + + async Task GetTask() + { + await Task.Delay(1); + if (canceledOnLinkedSource) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ctsOther.Token)) + { + var innerToken = cancelTokenSource.Token; + sourceThatWillBeCanceled.Cancel(); + innerToken.ThrowIfCancellationRequested(); + } + } + else + { + sourceThatWillBeCanceled.Cancel(); + sourceThatWillBeCanceled.Token.ThrowIfCancellationRequested(); + } + } + Task.WaitAll(otherTask, GetTask()); + } + }; + } + } +} diff --git a/tests/WrappedPolicyTests.cs b/tests/WrappedPolicyTests.cs index c5c19321..566a4061 100644 --- a/tests/WrappedPolicyTests.cs +++ b/tests/WrappedPolicyTests.cs @@ -959,11 +959,11 @@ public void Should_Return_Correct_PolicyResult_When_OperationCanceledException_O if (waitAll) { - pr = policy.Handle(GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + pr = policy.Handle(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); } else { - pr = policy.Handle(GetFuncWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + pr = policy.Handle(TaskWaitingDelegates.GetFuncWithTaskWait(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); @@ -978,67 +978,6 @@ public void Should_Return_Correct_PolicyResult_When_OperationCanceledException_O } } - private static Action GetFuncWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) - { - return () => - { - if (canceledOnLinkedSource) - { - using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) - { - var innerToken = cancelTokenSource.Token; - GetTask(innerToken).Wait(); - } - } - else - { - GetTask(sourceThatWillBeCanceled.Token).Wait(); - } - async Task GetTask(CancellationToken token) - { - await Task.Delay(1); - sourceThatWillBeCanceled.Cancel(); - token.ThrowIfCancellationRequested(); - } - }; - } - - private static Action GetFuncWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) - { - return () => - { - using (var ctsOther = new CancellationTokenSource()) - { - var otherTask = Task.Run(async () => - { - await Task.Delay(1); - ctsOther.Cancel(); - ctsOther.Token.ThrowIfCancellationRequested(); - }); - - async Task GetTask() - { - await Task.Delay(1); - if (canceledOnLinkedSource) - { - using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ctsOther.Token)) - { - var innerToken = cancelTokenSource.Token; - sourceThatWillBeCanceled.Cancel(); - innerToken.ThrowIfCancellationRequested(); - } - } - else - { - sourceThatWillBeCanceled.Cancel(); - sourceThatWillBeCanceled.Token.ThrowIfCancellationRequested(); - } - } - Task.WaitAll(otherTask, GetTask()); - } - }; - } - private class AlwaysFailedAndCanceledSimplePolicyProcessor : ISimplePolicyProcessor { private readonly bool _setCanceledExcepton; From 3d07ac4260e9634fdd6c2d96360fbf74f8f7522a Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 11 Feb 2026 11:22:30 +0300 Subject: [PATCH 056/109] Add tests for `SimplePolicyProcessor.Execute` with context to verify cancellation handling and that `ServiceOperationCanceledException` is stored in `PolicyResult` on one path. --- tests/SimplePolicyProcessorTests.cs | 32 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 0201424e..642ecced 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -1290,11 +1290,15 @@ void action(Exception _, ProcessingErrorInfo pi) } [Test] - [TestCase(true, true)] - [TestCase(false, true)] - [TestCase(true, false)] - [TestCase(false, false)] - public void Should_Execute_Action_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) + [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()) { @@ -1304,11 +1308,25 @@ public void Should_Execute_Action_Return_Correct_PolicyResult_When_OperationCanc if (waitAll) { - pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + if (withContext) + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), 5, cts.Token); + } + else + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + } } else { - pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + if (withContext) + { + pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), 5, 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); From 39930f402fd11278dd0d7f20d9e9f4221d95a2f5 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 12 Feb 2026 12:18:30 +0300 Subject: [PATCH 057/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `SimplePolicyProcessor.Execute` . --- src/Simple/SimplePolicyProcessor.cs | 2 +- tests/SimplePolicyProcessorTests.cs | 42 +++++++++++-- tests/TaskWaitingDelegates.cs | 92 +++++++++++++++++------------ tests/WrappedPolicyTests.cs | 4 +- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 8854b375..3ec4d5dc 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -87,7 +87,7 @@ 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) { diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 642ecced..9caad3d3 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -1289,6 +1289,40 @@ 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_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)] @@ -1310,22 +1344,22 @@ public void Should_Execute_Action_Return_Correct_PolicyResult_When_OperationCanc { if (withContext) { - pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), 5, cts.Token); + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), 5, cts.Token); } else { - pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); } } else { if (withContext) { - pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), 5, cts.Token); + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), 5, cts.Token); } else { - pr = processor.Execute(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + pr = processor.Execute(TaskWaitingDelegates.GetActionWithTaskWait(cts, canceledOnLinkedSource), cts.Token); } } Assert.That(pr.Errors.OfType().Count, Is.EqualTo(0)); diff --git a/tests/TaskWaitingDelegates.cs b/tests/TaskWaitingDelegates.cs index e01fc040..3027593f 100644 --- a/tests/TaskWaitingDelegates.cs +++ b/tests/TaskWaitingDelegates.cs @@ -6,7 +6,37 @@ namespace PoliNorError.Tests { internal static class TaskWaitingDelegates { - public static Action GetFuncWithTaskWait(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + 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 () => { @@ -15,56 +45,40 @@ public static Action GetFuncWithTaskWait(CancellationTokenSource sourceThatWillB using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) { var innerToken = cancelTokenSource.Token; - GetTask(innerToken).Wait(); + GetCanceledTask(innerToken, sourceThatWillBeCanceled).Wait(); } } else { - GetTask(sourceThatWillBeCanceled.Token).Wait(); - } - async Task GetTask(CancellationToken token) - { - await Task.Delay(1); - sourceThatWillBeCanceled.Cancel(); - token.ThrowIfCancellationRequested(); + GetCanceledTask(sourceThatWillBeCanceled.Token, sourceThatWillBeCanceled).Wait(); } }; } - public static Action GetFuncWithTaskWaitAll(CancellationTokenSource sourceThatWillBeCanceled, bool canceledOnLinkedSource) + private async static Task GetCanceledTask(CancellationToken tokenThatThrow, CancellationTokenSource sourceThatWillBeCanceled) { - return () => + 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 ctsOther = new CancellationTokenSource()) + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sourceThatWillBeCanceled.Token)) { - var otherTask = Task.Run(async () => - { - await Task.Delay(1); - ctsOther.Cancel(); - ctsOther.Token.ThrowIfCancellationRequested(); - }); - - async Task GetTask() - { - await Task.Delay(1); - if (canceledOnLinkedSource) - { - using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ctsOther.Token)) - { - var innerToken = cancelTokenSource.Token; - sourceThatWillBeCanceled.Cancel(); - innerToken.ThrowIfCancellationRequested(); - } - } - else - { - sourceThatWillBeCanceled.Cancel(); - sourceThatWillBeCanceled.Token.ThrowIfCancellationRequested(); - } - } - Task.WaitAll(otherTask, GetTask()); + 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 566a4061..81277297 100644 --- a/tests/WrappedPolicyTests.cs +++ b/tests/WrappedPolicyTests.cs @@ -959,11 +959,11 @@ public void Should_Return_Correct_PolicyResult_When_OperationCanceledException_O if (waitAll) { - pr = policy.Handle(TaskWaitingDelegates.GetFuncWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); + pr = policy.Handle(TaskWaitingDelegates.GetActionWithTaskWaitAll(cts, canceledOnLinkedSource), cts.Token); } else { - pr = policy.Handle(TaskWaitingDelegates.GetFuncWithTaskWait(cts, canceledOnLinkedSource), cts.Token); + 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); From fd338e349a7d803532cc14bba1d0eb29028a277b Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 13 Feb 2026 13:13:21 +0300 Subject: [PATCH 058/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `SimplePolicyProcessor.Execute` and `SimplePolicyProcessor.Execute` methods. --- src/Simple/SimplePolicyProcessor.cs | 2 +- tests/SimplePolicyProcessorTests.cs | 34 +++++++++++++++++++++++++++++ tests/TaskWaitingDelegates.cs | 10 +++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 3ec4d5dc..20123890 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -226,7 +226,7 @@ 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) { diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 9caad3d3..8280e5b7 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -1289,6 +1289,40 @@ 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)] diff --git a/tests/TaskWaitingDelegates.cs b/tests/TaskWaitingDelegates.cs index 3027593f..2dc0f4a3 100644 --- a/tests/TaskWaitingDelegates.cs +++ b/tests/TaskWaitingDelegates.cs @@ -6,6 +6,16 @@ namespace PoliNorError.Tests { internal static class TaskWaitingDelegates { + 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)(); From 39ddcfc95abac265cd01c8166e3959b60aa980a7 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 14 Feb 2026 19:46:01 +0300 Subject: [PATCH 059/109] Implement `ProcessingOrder.ProcessThenEvaluate` path in `PolicyProcessor.HandleExceptionAsync`. --- src/PolicyProcessor.cs | 33 +++++-- src/Simple/SimplePolicyProcessor.cs | 12 ++- .../PolicyProcessor.HandleException.Tests.cs | 5 +- ...icyProcessor.HandleExceptionAsync.Tests.cs | 96 ++++++++++++------- 4 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/PolicyProcessor.cs b/src/PolicyProcessor.cs index 7017c1fd..c08318dc 100644 --- a/src/PolicyProcessor.cs +++ b/src/PolicyProcessor.cs @@ -80,6 +80,7 @@ protected internal async Task HandleExceptionAsync( Func, bool, CancellationToken, Task> errorSaver, Func, CancellationToken, Task> policyRuleFunc, ExceptionHandlingBehavior handlingBehavior, + ProcessingOrder processingOrder, ErrorProcessingCancellationEffect cancellationEffect, bool configureAwait, CancellationToken token) @@ -117,19 +118,33 @@ protected internal async Task HandleExceptionAsync( } } - var ruleResult = await EvaluatePolicyRuleAsync(ex, policyResult, errorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); - if (ruleResult != ExceptionHandlingResult.Accepted) + if (processingOrder == ProcessingOrder.EvaluateThenProcess) { - return ruleResult; - } + 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) + 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 { - policyResult.SetFailedAndCanceled(); + 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); } - return ExceptionHandlingResult.Handled; } protected internal ExceptionHandlingResult HandleException( diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 20123890..9fdc91eb 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -528,7 +528,17 @@ private Task HandleExceptionAsync( CancellationToken token) { - return HandleExceptionAsync(ex, policyResult, errorContext, DefaultAsyncErrorSaver, DefaultAsyncPolicyRule, _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, configAwait, token); + return HandleExceptionAsync( + ex, + policyResult, + errorContext, + DefaultAsyncErrorSaver, + DefaultAsyncPolicyRule, + _rethrowIfErrorFilterUnsatisfied ? ExceptionHandlingBehavior.ConditionalRethrow : ExceptionHandlingBehavior.Handle, + ProcessingOrder.EvaluateThenProcess, + ErrorProcessingCancellationEffect.Propagate, + configAwait, + token); } } } diff --git a/tests/PolicyProcessor.HandleException.Tests.cs b/tests/PolicyProcessor.HandleException.Tests.cs index 8a19ca8b..77eeee1c 100644 --- a/tests/PolicyProcessor.HandleException.Tests.cs +++ b/tests/PolicyProcessor.HandleException.Tests.cs @@ -47,9 +47,10 @@ public Task TestHandleExceptionAsync( ExceptionHandlingBehavior handlingBehavior, ErrorProcessingCancellationEffect cancellationEffect, bool configureAwait, - CancellationToken token) + ProcessingOrder processingOrder = ProcessingOrder.EvaluateThenProcess, + CancellationToken token = default) { - return HandleExceptionAsync(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, cancellationEffect, configureAwait, token); + return HandleExceptionAsync(ex, policyResult, errorContext, errorSaver, policyRuleFunc, handlingBehavior, processingOrder, cancellationEffect, configureAwait, token); } } diff --git a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs index b7a5afff..57ea1d29 100644 --- a/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs +++ b/tests/PolicyProcessor.HandleExceptionAsync.Tests.cs @@ -26,8 +26,7 @@ public async Task Should_ReturnHandled_WhenHandlingBehaviorIsHandle() null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); } @@ -48,8 +47,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(policyResult.NoError, Is.False); } @@ -77,8 +75,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(errorSaverCalled, Is.True); } @@ -107,7 +104,8 @@ await processor.TestHandleExceptionAsync( ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, false, - cts.Token); + ProcessingOrder.EvaluateThenProcess, + cts.Token); } Assert.That(policyResult.IsFailed, Is.True); @@ -132,8 +130,7 @@ public async Task Should_ReturnRethrow_WhenPolicyRuleFuncReturnsFalseAndBehavior policyRuleFunc, ExceptionHandlingBehavior.ConditionalRethrow, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); Assert.That(policyResult.IsFailed, Is.True); @@ -156,8 +153,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(policyResult.IsSuccess, Is.True); } @@ -182,8 +178,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(policyResult.IsSuccess, Is.True); } @@ -208,8 +203,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Propagate, - false, - CancellationToken.None); + false); Assert.That(policyResult.IsFailed, Is.True); Assert.That(policyResult.IsCanceled, Is.True); @@ -235,8 +229,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(policyResult.IsCanceled, Is.False); } @@ -265,8 +258,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(errorSaverOrder, Is.EqualTo(1)); } @@ -294,8 +286,7 @@ await processor.TestHandleExceptionAsync( null, ExceptionHandlingBehavior.ConditionalRethrow, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(errorSaverCallCount, Is.EqualTo(1)); } @@ -316,8 +307,7 @@ public async Task Should_UseConfigureAwaitParameter_WhenCallingAsyncMethods() null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - true, - CancellationToken.None); + true); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); } @@ -342,6 +332,7 @@ await processor.TestHandleExceptionAsync( ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, false, + ProcessingOrder.EvaluateThenProcess, cts.Token); Assert.That(policyResult.IsSuccess, Is.True); @@ -372,7 +363,8 @@ await processor.TestHandleExceptionAsync( ExceptionHandlingBehavior.ConditionalRethrow, ErrorProcessingCancellationEffect.Ignore, false, - cts.Token); + ProcessingOrder.EvaluateThenProcess, + cts.Token); } Assert.That(policyResult.IsFailed, Is.True); @@ -397,8 +389,7 @@ public async Task Should_HandleExceptionAsync_ReturnHandled_WhenPolicyRuleFuncRe policyRuleFunc, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); } @@ -418,8 +409,7 @@ public async Task Should_ReturnRethrow_WhenExceptionFilterFailsAndBehaviorIsCond null, ExceptionHandlingBehavior.ConditionalRethrow, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Rethrow)); } @@ -438,8 +428,7 @@ public async Task Should_ReturnHandled_WhenExceptionFilterFailsAndBehaviorIsHand null, ExceptionHandlingBehavior.Handle, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + false); Assert.That(result, Is.EqualTo(ExceptionHandlingResult.Handled)); Assert.That(policyResult.IsFailed, Is.True); } @@ -462,12 +451,55 @@ public async Task Should_ReturnHandled_WhenExceptionFilterThrowsException(Except null, handlingBehavior, ErrorProcessingCancellationEffect.Ignore, - false, - CancellationToken.None); + 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)); + } + } } } From ae7c1340c08462997161c604a20e4dcbda709d5e Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 15 Feb 2026 21:15:55 +0300 Subject: [PATCH 060/109] Introduce `ExceptionFilter.IncludeError` method overloads. --- .../PolicyProcessor.ExceptionFilter.cs | 21 ++++++++++++ tests/ExceptionFilterTests.cs | 32 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs index 4d9ffddc..322261f7 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 { @@ -24,6 +25,26 @@ public ExceptionFilter ExcludeErrorSet(IErrorSet 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(); + } + } + internal ExceptionFilterSet FilterSet { get; } = new ExceptionFilterSet(); internal void AddIncludedErrorFilter(Expression> handledErrorFilter) diff --git a/tests/ExceptionFilterTests.cs b/tests/ExceptionFilterTests.cs index 87116c17..ff3a6165 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,37 @@ public void Should_FilledCatchBlockFilter_CreateByExcluding_Add_ErrorFilter_Expr Assert.That(filter.ErrorFilter.IncludedErrorFilters.Count(), Is.EqualTo(1)); } + [Test] + public void Should_Add_ErrorFilter_FromFunc() + { + var filter = new ExceptionFilter(); + filter.IncludeError((_) => true); + + Assert.That(filter.IncludedErrorFilters.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)] From 4ace2e7a3511b55222904bd5cd49086cb5cfeabe Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 16 Feb 2026 11:39:57 +0300 Subject: [PATCH 061/109] Introduce `ExceptionFilter.ExcludeError` method overloads. --- .../PolicyProcessor.ExceptionFilter.cs | 20 ++++++++++++++++ tests/ExceptionFilterTests.cs | 24 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs index 322261f7..a118105e 100644 --- a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs +++ b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs @@ -45,6 +45,26 @@ public ExceptionFilter IncludeError(Func func, Err } } + 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(); + } + } + internal ExceptionFilterSet FilterSet { get; } = new ExceptionFilterSet(); internal void AddIncludedErrorFilter(Expression> handledErrorFilter) diff --git a/tests/ExceptionFilterTests.cs b/tests/ExceptionFilterTests.cs index ff3a6165..caef385c 100644 --- a/tests/ExceptionFilterTests.cs +++ b/tests/ExceptionFilterTests.cs @@ -259,8 +259,10 @@ public void Should_Add_ErrorFilter_FromFunc() { var filter = new ExceptionFilter(); filter.IncludeError((_) => true); + filter.ExcludeError((_) => true); Assert.That(filter.IncludedErrorFilters.Count(), Is.EqualTo(1)); + Assert.That(filter.ExcludedErrorFilters.Count(), Is.EqualTo(1)); } [Test] @@ -285,6 +287,28 @@ public void Should_IncludeError_ForInnerError(bool errFilterUnsatisfied) 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)] From c932a7b265c95c77c0f661c33d2bca476472f36b Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 17 Feb 2026 14:11:58 +0300 Subject: [PATCH 062/109] Refactor `ErrorProcessorRegistration` methods to use expression-bodied syntax. --- ...orProcessorRegistration.ForErrorContext.cs | 62 +++++++------------ ...rrorProcessorRegistration.ForInnerError.cs | 48 ++++---------- 2 files changed, 36 insertions(+), 74 deletions(-) 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); } } From d7e4a51494f5f95bede03a0498833b4f670fe5de Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 17 Feb 2026 14:13:52 +0300 Subject: [PATCH 063/109] Refactor `ICanAddErrorProcessor` methods to use expression-bodied syntax. --- src/ErrorProcessors/ICanAddErrorProcessor.cs | 76 +++++--------------- 1 file changed, 19 insertions(+), 57 deletions(-) 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 { From 9b39b6d52f7e4f7d7c098c169f198ac0a21c802c Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 17 Feb 2026 14:21:38 +0300 Subject: [PATCH 064/109] Refactor `PolicyWrapper` and `PolicyWrapper` `PolicyDelegateResults` properties to use expression-bodied syntax. --- src/Wrap/PolicyWrapper.T.cs | 5 +---- src/Wrap/PolicyWrapper.cs | 15 ++++++--------- 2 files changed, 7 insertions(+), 13 deletions(-) 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; + } +} From c7ecf4b90ed85c446672332443f0d40aecd08451 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 17 Feb 2026 14:25:43 +0300 Subject: [PATCH 065/109] Remove duplicated implementations in FuncEntensions --- src/Utilities/FuncEntensions.cs | 37 ++++++++------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/src/Utilities/FuncEntensions.cs b/src/Utilities/FuncEntensions.cs index cb1ad8ce..972642f7 100644 --- a/src/Utilities/FuncEntensions.cs +++ b/src/Utilities/FuncEntensions.cs @@ -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) { From 5a193d9226ab5c0008ba6682ed69273e44fe08a6 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 18 Feb 2026 15:05:01 +0300 Subject: [PATCH 066/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `SimplePolicyProcessor.Execute`. --- src/Simple/SimplePolicyProcessor.cs | 2 +- tests/SimplePolicyProcessorTests.cs | 34 +++++++++++++++++++++++++++++ tests/TaskWaitingDelegates.cs | 10 +++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index 9fdc91eb..c9c5e534 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -185,7 +185,7 @@ 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) { diff --git a/tests/SimplePolicyProcessorTests.cs b/tests/SimplePolicyProcessorTests.cs index 8280e5b7..01b67b72 100644 --- a/tests/SimplePolicyProcessorTests.cs +++ b/tests/SimplePolicyProcessorTests.cs @@ -1323,6 +1323,40 @@ public void Should_Execute_Func_Return_Correct_PolicyResult_When_OperationCancel } } + [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)] diff --git a/tests/TaskWaitingDelegates.cs b/tests/TaskWaitingDelegates.cs index 2dc0f4a3..4ca3cfe0 100644 --- a/tests/TaskWaitingDelegates.cs +++ b/tests/TaskWaitingDelegates.cs @@ -6,6 +6,16 @@ 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; }; From effa83617812589ea6afacbb003d072522adbd27 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 19 Feb 2026 14:42:35 +0300 Subject: [PATCH 067/109] DRY refactor catch block into a private method used by `DefaultFallbackProcessor.Fallback` and `DefaultFallbackProcessor.Fallback`. --- src/Fallback/DefaultFallbackProcessor.cs | 29 ++++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index fe9be2df..81495750 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -85,8 +85,6 @@ private PolicyResult Fallback(Action action, Action fallback, result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { action(); @@ -102,12 +100,9 @@ private PolicyResult Fallback(Action action, Action fallback, } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); + bool policyRuleFunc(ErrorContext _, CancellationToken ct) { fallback(ct); return true; } - if (!result.IsFailed) - { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); - } + HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); } return result; } @@ -468,5 +463,25 @@ public DefaultFallbackProcessor AddErrorFilter(Func, CancellationToken, bool> policyRuleFunc, + CancellationToken token) + + { + HandleException( + ex, + policyResult, + errorContext, + DefaultErrorSaver, + policyRuleFunc, + ExceptionHandlingBehavior.Handle, + ProcessingOrder.ProcessThenEvaluate, + ErrorProcessingCancellationEffect.Propagate, + token); + } } } From 6ff7660eafd5e44364ee07966ed88c4ee83fd855 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 20 Feb 2026 10:21:59 +0300 Subject: [PATCH 068/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `DefaultFallbackProcessor.Fallback` and `DefaultFallbackProcessor.Fallback` methods. --- src/Fallback/DefaultFallbackProcessor.cs | 2 +- tests/DefaultFallbackProcessorTests.cs | 52 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 81495750..c8acbc19 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -96,7 +96,7 @@ 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) { diff --git a/tests/DefaultFallbackProcessorTests.cs b/tests/DefaultFallbackProcessorTests.cs index fb3377da..b3049e31 100644 --- a/tests/DefaultFallbackProcessorTests.cs +++ b/tests/DefaultFallbackProcessorTests.cs @@ -699,5 +699,57 @@ 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()); + } + } + } } } \ No newline at end of file From 39f09f88a2c7d7ae85087441e1dd46faae356e42 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 21 Feb 2026 19:15:24 +0300 Subject: [PATCH 069/109] Introduce `ExceptionFilter.IncludeError` and `ExcludeError` methods accepting `Expression>`. --- .../PolicyProcessor.ExceptionFilter.cs | 12 ++++++++++++ tests/ExceptionFilterTests.cs | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs index a118105e..23d96c5e 100644 --- a/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs +++ b/src/ExceptionFilter/PolicyProcessor.ExceptionFilter.cs @@ -45,6 +45,12 @@ public ExceptionFilter IncludeError(Func func, Err } } + public ExceptionFilter IncludeError(Expression> expression) + { + AddIncludedErrorFilter(expression); + return this; + } + public ExceptionFilter ExcludeError(ErrorType errorType = ErrorType.Error) where TException : Exception { return ExcludeError(null, errorType); @@ -65,6 +71,12 @@ public ExceptionFilter ExcludeError(Func func, Err } } + public ExceptionFilter ExcludeError(Expression> expression) + { + AddExcludedErrorFilter(expression); + return this; + } + internal ExceptionFilterSet FilterSet { get; } = new ExceptionFilterSet(); internal void AddIncludedErrorFilter(Expression> handledErrorFilter) diff --git a/tests/ExceptionFilterTests.cs b/tests/ExceptionFilterTests.cs index caef385c..f620f2db 100644 --- a/tests/ExceptionFilterTests.cs +++ b/tests/ExceptionFilterTests.cs @@ -255,11 +255,21 @@ public void Should_FilledCatchBlockFilter_CreateByExcluding_Add_ErrorFilter_Expr } [Test] - public void Should_Add_ErrorFilter_FromFunc() + [TestCase(true)] + [TestCase(false)] + public void Should_Add_ErrorFilter_FromFunc_Or_ByExpression(bool fromFunc) { var filter = new ExceptionFilter(); - filter.IncludeError((_) => true); - filter.ExcludeError((_) => true); + 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)); From 4563acd77786822435054bde03abd2828ce07999 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 22 Feb 2026 14:08:09 +0300 Subject: [PATCH 070/109] DRY refactor catch-block handling in `DefaultFallbackProcessor.Fallback`. --- src/Fallback/DefaultFallbackProcessor.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index c8acbc19..4e2992fd 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); @@ -60,12 +58,9 @@ private PolicyResult Fallback(Action action, TParam param, Actio } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); + bool policyRuleFunc(ErrorContext _, CancellationToken ct) { fallback(ct); return true; } - if (!result.IsFailed) - { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); - } + HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); } return result; } From 5e5b4eea93ac3d116dbe9087b0bb9fb8f8dd9643 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 23 Feb 2026 15:10:07 +0300 Subject: [PATCH 071/109] Add `MethodImpl(MethodImplOptions.AggressiveInlining)` to `PolicyResult.ForSync()` and `ForNotSync()`. --- src/PolicyResult.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PolicyResult.cs b/src/PolicyResult.cs index ac7bb77e..e0a35ed1 100644 --- a/src/PolicyResult.cs +++ b/src/PolicyResult.cs @@ -311,8 +311,12 @@ 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); + internal static new PolicyResult InitByConfigureAwait(bool configureAwait) => !configureAwait ? ForNotSync() : ForSync(); /// From b61de43d38ce5dc6cb8a6977a1db217b51c0a89e Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 24 Feb 2026 11:49:06 +0300 Subject: [PATCH 072/109] DRY refactor internal `DefaultErrorProcessorT` class. --- .../DefaultErrorProcessor.T.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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 => (_) => _; } } From 663402f691b547e99388c227b5c25cd95d53b4ba Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 24 Feb 2026 14:38:57 +0300 Subject: [PATCH 073/109] Add `ConvertExceptionDelegates.ToSubException` static method to internal `ConvertExceptionDelegates` class. --- src/ConvertExceptionDelegates.cs | 12 ++++ tests/ConvertExceptionDelegatesTests.cs | 74 +++++++++++++++++++++++++ tests/PoliNorError.Tests.csproj | 1 + 3 files changed, 87 insertions(+) create mode 100644 tests/ConvertExceptionDelegatesTests.cs 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/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/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 1b818981..7dafdcc4 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -93,6 +93,7 @@ + From 6090fe3a33ff1bbea2e2808add59df07a5c562fb Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 25 Feb 2026 13:14:04 +0300 Subject: [PATCH 074/109] Introduce `DefaultTypedErrorProcessor` for typed exception handling. --- .../DefaultTypedErrorProcessor.cs | 40 +++++++++++++++++++ tests/ErrorProcessorTests.cs | 34 ++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/ErrorProcessors/DefaultTypedErrorProcessor.cs 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/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)); + } } } From 60aba652e6fca6fce6865583bba40f60a03e9c1c Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 26 Feb 2026 14:23:50 +0300 Subject: [PATCH 075/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `DefaultFallbackProcessor.Fallback`. --- src/Fallback/DefaultFallbackProcessor.cs | 2 +- tests/DefaultFallbackProcessorTests.cs | 34 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 4e2992fd..70004757 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -54,7 +54,7 @@ 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) { diff --git a/tests/DefaultFallbackProcessorTests.cs b/tests/DefaultFallbackProcessorTests.cs index b3049e31..7f6c37c5 100644 --- a/tests/DefaultFallbackProcessorTests.cs +++ b/tests/DefaultFallbackProcessorTests.cs @@ -751,5 +751,39 @@ public void Should_Fallback_Action_Return_Correct_PolicyResult_When_OperationCan } } } + + [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 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()); + } + } + } } } \ No newline at end of file From 021d262643c98ecf7656ed6a971d92cb55e52e34 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 27 Feb 2026 13:38:22 +0300 Subject: [PATCH 076/109] DRY refactor catch block into a private method used by `DefaultFallbackProcessor.Fallback` and `DefaultFallbackProcessor.Fallback`. --- src/Fallback/DefaultFallbackProcessor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 70004757..d0b1f8ec 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -176,8 +176,6 @@ private PolicyResult Fallback(Func func, Func fal result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(); @@ -194,12 +192,14 @@ private PolicyResult Fallback(Func func, Func fal } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); - - if (!result.IsFailed) + bool policyRuleFunc(ErrorContext _, CancellationToken ct) { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); + var fallbackResult = fallback(ct); + result.SetResult(fallbackResult); + return true; } + + HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); } return result; } From 9fb375fe7b5caf87a54d7616f2684d4aae8a0ce4 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 28 Feb 2026 14:27:12 +0300 Subject: [PATCH 077/109] Introduce abstract `TypedErrorProcessor` with an overridable `Execute` method for type-specific error processing. --- src/ErrorProcessors/TypedErrorProcessor.cs | 84 +++++++ tests/PoliNorError.Tests.csproj | 1 + tests/SyncErrorProcessorTParamTests.cs | 4 +- tests/SyncTypedErrorProcessor.cs | 280 +++++++++++++++++++++ 4 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 src/ErrorProcessors/TypedErrorProcessor.cs create mode 100644 tests/SyncTypedErrorProcessor.cs 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/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 7dafdcc4..82c7cdf7 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -130,6 +130,7 @@ + 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>()); + } + } +} From 7d6ebf993cf9cc59ec82629fa0b5191b5aa449f9 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 1 Mar 2026 07:51:42 +0300 Subject: [PATCH 078/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `DefaultFallbackProcessor.Fallback` and `DefaultFallbackProcessor.Fallback` methods. --- src/Fallback/DefaultFallbackProcessor.cs | 2 +- tests/DefaultFallbackProcessorTests.cs | 37 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index d0b1f8ec..9047e754 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -188,7 +188,7 @@ 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) { diff --git a/tests/DefaultFallbackProcessorTests.cs b/tests/DefaultFallbackProcessorTests.cs index 7f6c37c5..c8e123f5 100644 --- a/tests/DefaultFallbackProcessorTests.cs +++ b/tests/DefaultFallbackProcessorTests.cs @@ -757,7 +757,7 @@ public void Should_Fallback_Action_Return_Correct_PolicyResult_When_OperationCan [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) + public void Should_Fallback_ActionWithParam_Return_Correct_PolicyResult_When_OperationCanceledException_On_Linked_Token(bool canceledOnLinkedSource, bool waitAll) { using (var cts = new CancellationTokenSource()) { @@ -785,5 +785,40 @@ public void Should_Execute_ActionWithParam_Return_Correct_PolicyResult_When_Oper } } } + + [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()); + } + } + } + } } \ No newline at end of file From 06d984766f4bc6005dfd2d6523691d94ccdf97d0 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 2 Mar 2026 14:52:22 +0300 Subject: [PATCH 079/109] DRY refactor catch block into a private method used by `DefaultFallbackProcessor.FallbackAsync` and `DefaultFallbackProcessor.FallbackAsync`. --- src/Fallback/DefaultFallbackProcessor.cs | 33 +++++++++++++++++++----- tests/DefaultFallbackProcessorTests.cs | 1 - 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 9047e754..7ad9f82e 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -283,8 +283,6 @@ private async Task FallbackAsync(Func fun result.SetExecuted(); - var exHandler = new SimpleAsyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), configureAwait, token); - try { await func(token).ConfigureAwait(configureAwait); @@ -296,12 +294,13 @@ private async Task FallbackAsync(Func fun } catch (Exception ex) { - await exHandler.HandleAsync(ex, emptyErrorContext).ConfigureAwait(configureAwait); - - if (!result.IsFailed) + async Task policyRuleFunc(ErrorContext _, CancellationToken ct) { - (await fallback.HandleAsFallbackAsync(configureAwait, token).ConfigureAwait(configureAwait)).ChangePolicyResult(result, ex); + await fallback(ct).ConfigureAwait(configureAwait); + return true; } + + await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); } return result; } @@ -478,5 +477,27 @@ private void HandleException( ErrorProcessingCancellationEffect.Propagate, token); } + + 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/tests/DefaultFallbackProcessorTests.cs b/tests/DefaultFallbackProcessorTests.cs index c8e123f5..6ca23745 100644 --- a/tests/DefaultFallbackProcessorTests.cs +++ b/tests/DefaultFallbackProcessorTests.cs @@ -819,6 +819,5 @@ public void Should_Fallback_Func_Return_Correct_PolicyResult_When_OperationCance } } } - } } \ No newline at end of file From c0d096cc5e5248021150f5b983acdda41bd39e0e Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 3 Mar 2026 15:21:34 +0300 Subject: [PATCH 080/109] Add implicit conversion from `ConstantRetryDelayOptions` to `ConstantRetryDelay`. --- src/Retry/ConstantRetryDelay.cs | 1 + tests/RetryDelay.Tests/RetryDelayTests.cs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Retry/ConstantRetryDelay.cs b/src/Retry/ConstantRetryDelay.cs index 3102a601..74302d56 100644 --- a/src/Retry/ConstantRetryDelay.cs +++ b/src/Retry/ConstantRetryDelay.cs @@ -68,5 +68,6 @@ private static double GetDelayValueInMs(ConstantRetryDelayOptions options) public class ConstantRetryDelayOptions : RetryDelayOptions { public override RetryDelayType DelayType => RetryDelayType.Constant; + public static implicit operator ConstantRetryDelay(ConstantRetryDelayOptions options) => new ConstantRetryDelay(options); } } diff --git a/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index 2ca6c0d9..cd297557 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -290,5 +290,21 @@ 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))); + } + + private class RetryDelayTester + { + public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) + { + return retryDelay.GetDelay(attemptNumber); + } + } } } From e12e80bbcb7576d39957c8320e13f252dd5d7565 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 4 Mar 2026 16:05:31 +0300 Subject: [PATCH 081/109] Add implicit conversion from `LinearRetryDelayOptions` to `LinearRetryDelay`. --- src/Retry/ConstantRetryDelay.cs | 1 + src/Retry/LinearRetryDelay.cs | 2 ++ tests/RetryDelay.Tests/RetryDelayTests.cs | 8 ++++++++ 3 files changed, 11 insertions(+) diff --git a/src/Retry/ConstantRetryDelay.cs b/src/Retry/ConstantRetryDelay.cs index 74302d56..b730dc44 100644 --- a/src/Retry/ConstantRetryDelay.cs +++ b/src/Retry/ConstantRetryDelay.cs @@ -68,6 +68,7 @@ private static double GetDelayValueInMs(ConstantRetryDelayOptions options) public class ConstantRetryDelayOptions : RetryDelayOptions { public override RetryDelayType DelayType => RetryDelayType.Constant; + public static implicit operator ConstantRetryDelay(ConstantRetryDelayOptions options) => new ConstantRetryDelay(options); } } diff --git a/src/Retry/LinearRetryDelay.cs b/src/Retry/LinearRetryDelay.cs index 526223eb..55e978ff 100644 --- a/src/Retry/LinearRetryDelay.cs +++ b/src/Retry/LinearRetryDelay.cs @@ -87,5 +87,7 @@ 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); } } diff --git a/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index cd297557..90c82464 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -299,6 +299,14 @@ public void Should_Implicitly_Convert_ConstantRetryDelayOptions_To_RetryDelay() 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))); + } + private class RetryDelayTester { public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) From 98c58590e31524b09d5f76776e71d171bc2a7884 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 5 Mar 2026 14:14:48 +0300 Subject: [PATCH 082/109] Add implicit conversion from `ExponentialRetryDelayOptions` to `ExponentialRetryDelay`. --- src/Retry/ExponentialRetryDelay.cs | 2 ++ tests/RetryDelay.Tests/RetryDelayTests.cs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/Retry/ExponentialRetryDelay.cs b/src/Retry/ExponentialRetryDelay.cs index 58c14aeb..26187522 100644 --- a/src/Retry/ExponentialRetryDelay.cs +++ b/src/Retry/ExponentialRetryDelay.cs @@ -82,5 +82,7 @@ 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); } } diff --git a/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index 90c82464..9c6b1640 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -307,6 +307,14 @@ public void Should_Implicitly_Convert_LinearRetryDelayOptions_To_RetryDelay() 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)))); + } + private class RetryDelayTester { public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) From c559d78945f990845630dcdd6856ceca36e065d7 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 6 Mar 2026 11:21:23 +0300 Subject: [PATCH 083/109] Store `ServiceOperationCanceledException` in `PolicyResult.PolicyCanceledError` when cancellation is observed on linked token during synchronous wait on tasks in `DefaultFallbackProcessor.Fallback`. --- src/Fallback/DefaultFallbackProcessor.cs | 2 +- tests/DefaultFallbackProcessorTests.cs | 34 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 7ad9f82e..7b6dc027 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -147,7 +147,7 @@ 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) { diff --git a/tests/DefaultFallbackProcessorTests.cs b/tests/DefaultFallbackProcessorTests.cs index 6ca23745..c1c605ff 100644 --- a/tests/DefaultFallbackProcessorTests.cs +++ b/tests/DefaultFallbackProcessorTests.cs @@ -819,5 +819,39 @@ public void Should_Fallback_Func_Return_Correct_PolicyResult_When_OperationCance } } } + + [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 From 9a038e28e6be4e772521caedd9c512c949231349 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 7 Mar 2026 16:13:01 +0300 Subject: [PATCH 084/109] DRY refactor catch-block handling in `DefaultFallbackProcessor.Fallback`. --- src/Fallback/DefaultFallbackProcessor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 7b6dc027..c245c8da 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -133,8 +133,6 @@ private PolicyResult Fallback(Func func, TParam param, result.SetExecuted(); - var exHandler = new SimpleSyncExceptionHandler(result, _bulkErrorProcessor, ErrorFilter.GetCanHandle(), token); - try { var resAction = func(param); @@ -151,12 +149,14 @@ private PolicyResult Fallback(Func func, TParam param, } catch (Exception ex) { - exHandler.Handle(ex, emptyErrorContext); - - if (!result.IsFailed) + bool policyRuleFunc(ErrorContext _, CancellationToken ct) { - fallback.HandleAsFallback(token).ChangePolicyResult(result, ex); + var fallbackResult = fallback(ct); + result.SetResult(fallbackResult); + return true; } + + HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); } return result; } From e7ec42e0be6867a9849bbaa9d894d2d780388cf3 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 8 Mar 2026 19:36:15 +0300 Subject: [PATCH 085/109] DRY refactor catch-block handling in `DefaultFallbackProcessor.FallbackAsync`. --- src/Fallback/DefaultFallbackProcessor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index c245c8da..70f53b85 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -258,12 +258,13 @@ private async Task FallbackAsync(Func policyRuleFunc(ErrorContext _, CancellationToken ct) { - (await fallback.HandleAsFallbackAsync(configureAwait, token).ConfigureAwait(configureAwait)).ChangePolicyResult(result, ex); + await fallback(ct).ConfigureAwait(configureAwait); + return true; } + + await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); } return result; } From e7cbe8adda0515ac47f13cee92003d2f013f5540 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 9 Mar 2026 16:18:32 +0300 Subject: [PATCH 086/109] Add `FromErrors` and `FromErrors` methods to `ErrorSet`. --- src/ErrorSet.cs | 28 +++++++++++++++ tests/ErrorSetTests.cs | 79 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/ErrorSet.cs b/src/ErrorSet.cs index 8ae3a1a7..3877da0d 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. /// diff --git a/tests/ErrorSetTests.cs b/tests/ErrorSetTests.cs index 12020f66..25bdf794 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,72 @@ 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)); + } + } } From 84d631e0b487d25547844d0349fd58cb1197efca Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Mar 2026 11:28:17 +0300 Subject: [PATCH 087/109] DRY refactor catch block handling in `DefaultFallbackProcessor` `Fallback`, `Fallback`, and `Fallback` methods. --- src/Fallback/DefaultFallbackProcessor.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 70f53b85..578bc781 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -58,9 +58,7 @@ private PolicyResult Fallback(Action action, TParam param, Actio } catch (Exception ex) { - bool policyRuleFunc(ErrorContext _, CancellationToken ct) { fallback(ct); return true; } - - HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -95,9 +93,7 @@ private PolicyResult Fallback(Action action, Action fallback, } catch (Exception ex) { - bool policyRuleFunc(ErrorContext _, CancellationToken ct) { fallback(ct); return true; } - - HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -245,8 +241,6 @@ private async Task FallbackAsync(Func 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, From bdfb88109552980f93c54a400e54237af5db9046 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Mar 2026 13:00:13 +0300 Subject: [PATCH 088/109] Add `HasError` and `HasInnerError` methods to `ErrorSet`. --- src/ErrorSet.cs | 36 ++++++++++++++++++++++++++++++++++++ tests/ErrorSetTests.cs | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/ErrorSet.cs b/src/ErrorSet.cs index 3877da0d..1140a20f 100644 --- a/src/ErrorSet.cs +++ b/src/ErrorSet.cs @@ -98,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/tests/ErrorSetTests.cs b/tests/ErrorSetTests.cs index 25bdf794..aa0cadc6 100644 --- a/tests/ErrorSetTests.cs +++ b/tests/ErrorSetTests.cs @@ -109,5 +109,47 @@ public void Should_NotAddDuplicateItems_When_CallingFromErrorsWithSameTypeMultip 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); + } } } From 47f1b33584e8256a9cc3073419552da30f0a7288 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 11 Mar 2026 14:27:36 +0300 Subject: [PATCH 089/109] Add implicit conversion from `TimeSeriesRetryDelayOptions` to `TimeSeriesRetryDelay`. --- src/Retry/TimeSeriesRetryDelayOptions.cs | 2 ++ tests/RetryDelay.Tests/RetryDelayTests.cs | 8 ++++++++ 2 files changed, 10 insertions(+) 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/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index 9c6b1640..4146e920 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -315,6 +315,14 @@ public void Should_Implicitly_Convert_ExponentialRetryDelayOptions_To_RetryDelay 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))); + } + private class RetryDelayTester { public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) From 43fff4afffaeb7f94ac6e6d016ff0d119d1d5ddd Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 12 Mar 2026 18:22:11 +0300 Subject: [PATCH 090/109] DRY refactor catch-block handling in `DefaultFallbackProcessor.FallbackAsync`. --- src/Fallback/DefaultFallbackProcessor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 578bc781..0f7e14f6 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -341,8 +341,6 @@ private async Task> FallbackAsync(Func> FallbackAsync(Func policyRuleFunc(ErrorContext _, CancellationToken ct) { - (await fallback.HandleAsFallbackAsync(configureAwait, token).ConfigureAwait(configureAwait)).ChangePolicyResult(result, ex); + var fallbackResult = await fallback(ct).ConfigureAwait(configureAwait); + result.SetResult(fallbackResult); + return true; } + + await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); } return result; } From 86373a630a4098fe41bcada9b71ec7a021569040 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 13 Mar 2026 16:53:18 +0300 Subject: [PATCH 091/109] DRY refactor catch block into a private method used by `DefaultFallbackProcessor.FallbackAsync` and `DefaultFallbackProcessor.FallbackAsync`. --- src/Fallback/DefaultFallbackProcessor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 0f7e14f6..675e3b5d 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -380,8 +380,6 @@ private async Task> FallbackAsync(Func> FallbackAsync(Func policyRuleFunc(ErrorContext _, CancellationToken ct) { - (await fallback.HandleAsFallbackAsync(configureAwait, token).ConfigureAwait(configureAwait)).ChangePolicyResult(result, ex); + var fallbackResult = await fallback(ct).ConfigureAwait(configureAwait); + result.SetResult(fallbackResult); + return true; } + + await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); } return result; } From 5cd00d57dae157ed376d5f37889fbdda44704313 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 14 Mar 2026 18:54:41 +0300 Subject: [PATCH 092/109] Add `MethodImpl(MethodImplOptions.AggressiveInlining)` to `PolicyResult.InitByConfigureAwait` and `PolicyResult.InitByConfigureAwait` methods. --- src/PolicyResult.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PolicyResult.cs b/src/PolicyResult.cs index e0a35ed1..b1dc238d 100644 --- a/src/PolicyResult.cs +++ b/src/PolicyResult.cs @@ -24,6 +24,7 @@ public class PolicyResult [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static PolicyResult ForNotSync() => new PolicyResult(true); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static PolicyResult InitByConfigureAwait(bool configureAwait) => !configureAwait ? ForNotSync() : ForSync(); /// @@ -317,6 +318,7 @@ public class PolicyResult : 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(); /// From bc7b65a2839588741fecdb7a8cee1b7ebb415339 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 15 Mar 2026 06:14:40 +0300 Subject: [PATCH 093/109] Remove unused private `SimplePolicyProcessor.GetFilterUnsatisfiedOrFilterException` method --- src/Simple/SimplePolicyProcessor.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Simple/SimplePolicyProcessor.cs b/src/Simple/SimplePolicyProcessor.cs index c9c5e534..635f6f74 100644 --- a/src/Simple/SimplePolicyProcessor.cs +++ b/src/Simple/SimplePolicyProcessor.cs @@ -475,18 +475,6 @@ public SimplePolicyProcessor WithErrorContextProcessor(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) { From ebee27fcc70b6d6b773fc3e4562ca20bed5f934e Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 16 Mar 2026 11:20:58 +0300 Subject: [PATCH 094/109] DRY refactor catch block handling in `DefaultFallbackProcessor` `Fallback`, `Fallback`, and `Fallback` methods. --- src/Fallback/DefaultFallbackProcessor.cs | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 675e3b5d..6193ce96 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -145,14 +145,7 @@ private PolicyResult Fallback(Func func, TParam param, } catch (Exception ex) { - bool policyRuleFunc(ErrorContext _, CancellationToken ct) - { - var fallbackResult = fallback(ct); - result.SetResult(fallbackResult); - return true; - } - - HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -188,14 +181,7 @@ private PolicyResult Fallback(Func func, Func fal } catch (Exception ex) { - bool policyRuleFunc(ErrorContext _, CancellationToken ct) - { - var fallbackResult = fallback(ct); - result.SetResult(fallbackResult); - return true; - } - - HandleException(ex, result, emptyErrorContext, policyRuleFunc, token); + HandleException(fallback, emptyErrorContext, result, ex, token); } return result; } @@ -453,6 +439,23 @@ public DefaultFallbackProcessor AddErrorFilter(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, From 4dba312ce8af3ddb4d77aff837315e71e85beeb4 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 17 Mar 2026 13:08:52 +0300 Subject: [PATCH 095/109] DRY refactor catch block into a private method used by `DefaultFallbackProcessor.FallbackAsync`, `FallbackAsync` and `FallbackAsync`. --- src/Fallback/DefaultFallbackProcessor.cs | 34 ++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 6193ce96..055243df 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -238,13 +238,7 @@ private async Task FallbackAsync(Func policyRuleFunc(ErrorContext _, CancellationToken ct) - { - await fallback(ct).ConfigureAwait(configureAwait); - return true; - } - - await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + await HandleExceptionAsync(fallback, emptyErrorContext, result, ex, configureAwait, token).ConfigureAwait(configureAwait); } return result; } @@ -275,13 +269,7 @@ private async Task FallbackAsync(Func fun } catch (Exception ex) { - async Task policyRuleFunc(ErrorContext _, CancellationToken ct) - { - await fallback(ct).ConfigureAwait(configureAwait); - return true; - } - - await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + await HandleExceptionAsync(fallback, emptyErrorContext, result, ex, configureAwait, token).ConfigureAwait(configureAwait); } return result; } @@ -488,6 +476,24 @@ private void HandleException( token); } + 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; + } + + await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + } + + private Task HandleExceptionAsync( Exception ex, PolicyResult policyResult, From 6cf823ea7ec4693c1c96f110e4bf50b5a7d43940 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 18 Mar 2026 14:45:38 +0300 Subject: [PATCH 096/109] Edit PoliNorError.Tests.csproj. --- tests/PoliNorError.Tests.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 82c7cdf7..5878c725 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -6,6 +6,7 @@ + Debug @@ -188,8 +189,6 @@ - - \ No newline at end of file From 05a0a0cbc27d29b6600ac3244ae478bc8013eb19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:48:04 +0000 Subject: [PATCH 097/109] Bump System.ValueTuple from 4.6.1 to 4.6.2 --- updated-dependencies: - dependency-name: System.ValueTuple dependency-version: 4.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- tests/App.config | 4 ++-- tests/PoliNorError.Tests.csproj | 21 +++++++++++---------- tests/packages.config | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) 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/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 5878c725..4e73e97e 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -1,12 +1,11 @@  - - - - - - + + + + + Debug @@ -185,10 +184,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/packages.config b/tests/packages.config index c7f79049..22ae21d9 100644 --- a/tests/packages.config +++ b/tests/packages.config @@ -9,5 +9,5 @@ - + \ No newline at end of file From c579b57be553da234ad4de439713ee19f5121623 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 20 Mar 2026 10:47:38 +0300 Subject: [PATCH 098/109] DRY refactor catch block into a private method used by `DefaultFallbackProcessor.FallbackAsync`, `FallbackAsync` and `FallbackAsync`. --- src/Fallback/DefaultFallbackProcessor.cs | 40 +++++++++++++----------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 055243df..716be9db 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -327,14 +327,7 @@ private async Task> FallbackAsync(Func policyRuleFunc(ErrorContext _, CancellationToken ct) - { - var fallbackResult = await fallback(ct).ConfigureAwait(configureAwait); - result.SetResult(fallbackResult); - return true; - } - - await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + await HandleExceptionAsync(fallback, emptyErrorContext, result, ex, configureAwait, token).ConfigureAwait(configureAwait); } return result; } @@ -366,14 +359,7 @@ private async Task> FallbackAsync(Func policyRuleFunc(ErrorContext _, CancellationToken ct) - { - var fallbackResult = await fallback(ct).ConfigureAwait(configureAwait); - result.SetResult(fallbackResult); - return true; - } - - await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + await HandleExceptionAsync(fallback, emptyErrorContext, result, ex, configureAwait, token).ConfigureAwait(configureAwait); } return result; } @@ -476,7 +462,25 @@ private void HandleException( token); } - private async Task HandleExceptionAsync( + 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, @@ -490,7 +494,7 @@ async Task policyRuleFunc(ErrorContext _, CancellationToken ct) return true; } - await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); + return await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); } From 7f16de90d3f85016dd0485f72f7d4d2d5acfab67 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 22 Mar 2026 21:27:28 +0300 Subject: [PATCH 099/109] Update NUnit to 4.5.1. --- tests/PoliNorError.Tests.csproj | 12 ++++++------ tests/packages.config | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 4e73e97e..8d60c5c5 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -1,6 +1,6 @@  - + @@ -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 @@ -188,8 +188,8 @@ - + \ No newline at end of file diff --git a/tests/packages.config b/tests/packages.config index 22ae21d9..a1e4e416 100644 --- a/tests/packages.config +++ b/tests/packages.config @@ -2,7 +2,7 @@ - + From cb9217be75020c390c405871d574d2fcf9de4bb9 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 23 Mar 2026 16:09:38 +0300 Subject: [PATCH 100/109] Eliminate per-call `MaxDelayDelimiter` allocations in `ConstantRetryDelay`. --- src/Retry/ConstantRetryDelay.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Retry/ConstantRetryDelay.cs b/src/Retry/ConstantRetryDelay.cs index b730dc44..ef6b3e19 100644 --- a/src/Retry/ConstantRetryDelay.cs +++ b/src/Retry/ConstantRetryDelay.cs @@ -26,7 +26,8 @@ private static Func GetDelayValueProvider(ConstantRetryDelayOptio { if (retryDelayOptions.UseJitter) { - return GetJitteredDelayValue(retryDelayOptions); + var maxDelayDelimiter = new MaxDelayDelimiter(retryDelayOptions); + return (_) => maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(ApplyJitter(GetDelayValueInMs(retryDelayOptions))); } else { @@ -47,15 +48,6 @@ internal ConstantRetryDelay(TimeSpan baseDelay, TimeSpan? maxDelay = null, bool private static Func GetDelayValue(ConstantRetryDelayOptions options) => (_) => options.BaseDelay; - private static Func GetJitteredDelayValue(ConstantRetryDelayOptions options) - { - return (_) => - { - var maxDelayDelimiter = new MaxDelayDelimiter(options); - return maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(ApplyJitter(GetDelayValueInMs(options))); - }; - } - private static double GetDelayValueInMs(ConstantRetryDelayOptions options) { return options.BaseDelay.TotalMilliseconds; From b7251f8c2c016b9569ade704e66e99f0cfbfa8f1 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 24 Mar 2026 13:26:45 +0300 Subject: [PATCH 101/109] Add internal `StandardJitter` class. --- src/Retry/StandardJitter.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Retry/StandardJitter.cs 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; + } + } +} From 2db0e05e40954fc708b0879aaa1c3e237123fbc0 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 24 Mar 2026 16:11:44 +0300 Subject: [PATCH 102/109] Add `ConstantDelayCore` and simplify `ConstantRetryDelay` constructor initialization. --- src/Retry/ConstantRetryDelay.cs | 56 ++++++++++++++--------- tests/RetryDelay.Tests/RetryDelayTests.cs | 52 +++++++++++++++++++++ 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/src/Retry/ConstantRetryDelay.cs b/src/Retry/ConstantRetryDelay.cs index ef6b3e19..77d17e6f 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).GetDelay) { if (retryDelayOptions.UseJitter && retryDelayOptions.MaxDelay < retryDelayOptions.BaseDelay) { @@ -22,19 +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) - { - var maxDelayDelimiter = new MaxDelayDelimiter(retryDelayOptions); - return (_) => maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(ApplyJitter(GetDelayValueInMs(retryDelayOptions))); - } - else - { - return GetDelayValue(retryDelayOptions); - } - } - /// /// Creates . /// @@ -45,13 +32,6 @@ 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 }){} - - private static Func GetDelayValue(ConstantRetryDelayOptions options) => (_) => options.BaseDelay; - - private static double GetDelayValueInMs(ConstantRetryDelayOptions options) - { - return options.BaseDelay.TotalMilliseconds; - } } /// @@ -63,4 +43,38 @@ public class ConstantRetryDelayOptions : RetryDelayOptions public static implicit operator ConstantRetryDelay(ConstantRetryDelayOptions options) => new ConstantRetryDelay(options); } + + internal class ConstantDelayCore + { + private readonly ConstantRetryDelayOptions _delayOptions; + private readonly MaxDelayDelimiter _maxDelayDelimiter; + + private readonly Func _getDelay; + + public ConstantDelayCore(ConstantRetryDelayOptions delayOptions) + { + _delayOptions = delayOptions; + if (delayOptions.UseJitter) + { + _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); + _getDelay = GetJitteredDelay; + } + else + { + _getDelay = GetBaseDelay; + } + } + + public TimeSpan GetDelay(int attempt) => _getDelay(attempt); + + private TimeSpan GetBaseDelay(int attempt) + { + return _delayOptions.BaseDelay; + } + + private TimeSpan GetJitteredDelay(int attempt) + { + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(StandardJitter.AddJitter(_delayOptions.BaseDelay.TotalMilliseconds)); + } + } } diff --git a/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index 4146e920..c10a7c79 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -323,6 +323,58 @@ public void Should_Implicitly_Convert_TimeSeriesRetryDelayOptions_To_RetryDelay( 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))); + } + private class RetryDelayTester { public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) From 0c7f5b1e5160f6da9d31db1972e190c6bff45062 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 25 Mar 2026 14:05:44 +0300 Subject: [PATCH 103/109] Add `LinearDelayCore` and simplify `LinearRetryDelay` constructor initialization. --- src/Retry/LinearRetryDelay.cs | 78 ++++++++++++----------- tests/RetryDelay.Tests/RetryDelayTests.cs | 52 +++++++++++++++ 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/Retry/LinearRetryDelay.cs b/src/Retry/LinearRetryDelay.cs index 55e978ff..0266f5ca 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).GetDelay) { #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; - } } /// @@ -90,4 +54,44 @@ public class LinearRetryDelayOptions : RetryDelayOptions public static implicit operator LinearRetryDelay(LinearRetryDelayOptions options) => new LinearRetryDelay(options); } + + internal class LinearDelayCore + { + private readonly LinearRetryDelayOptions _delayOptions; + private readonly MaxDelayDelimiter _maxDelayDelimiter; + + private readonly Func _getDelay; + + public LinearDelayCore(LinearRetryDelayOptions delayOptions) + { + _delayOptions = delayOptions; + _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); + + if (delayOptions.UseJitter) + { + _getDelay = GetJitteredDelay; + } + else + { + _getDelay = GetBaseDelay; + } + } + + public TimeSpan GetDelay(int attempt) => _getDelay(attempt); + + private TimeSpan GetBaseDelay(int attempt) + { + return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(GetDelayValueInMs(attempt, _delayOptions)); + } + + private 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/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index c10a7c79..bfa630d9 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -375,6 +375,58 @@ public void Should_Create_ConstantRetryDelay_Instance_Via_Create_Method() 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))); + } + private class RetryDelayTester { public TimeSpan GetAttemptDelay(RetryDelay retryDelay, int attemptNumber = 0) From 4c1ed84217ec8a7322c58a7ddf5bc0dc2c8f0ee5 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 26 Mar 2026 14:13:23 +0300 Subject: [PATCH 104/109] Extract `DelayCoreBase` abstract base class and use it in `ConstantRetryDelay` to simplify constructor initialization. --- src/Retry/ConstantRetryDelay.cs | 19 +++++-------------- src/Retry/DelayCoreBase.cs | 25 +++++++++++++++++++++++++ src/Retry/RetryDelay.cs | 2 ++ 3 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 src/Retry/DelayCoreBase.cs diff --git a/src/Retry/ConstantRetryDelay.cs b/src/Retry/ConstantRetryDelay.cs index 77d17e6f..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(new ConstantDelayCore(retryDelayOptions).GetDelay) + public ConstantRetryDelay(ConstantRetryDelayOptions retryDelayOptions) : base(new ConstantDelayCore(retryDelayOptions)) { if (retryDelayOptions.UseJitter && retryDelayOptions.MaxDelay < retryDelayOptions.BaseDelay) { @@ -44,35 +44,26 @@ public class ConstantRetryDelayOptions : RetryDelayOptions public static implicit operator ConstantRetryDelay(ConstantRetryDelayOptions options) => new ConstantRetryDelay(options); } - internal class ConstantDelayCore + internal class ConstantDelayCore : DelayCoreBase { private readonly ConstantRetryDelayOptions _delayOptions; private readonly MaxDelayDelimiter _maxDelayDelimiter; - private readonly Func _getDelay; - - public ConstantDelayCore(ConstantRetryDelayOptions delayOptions) + public ConstantDelayCore(ConstantRetryDelayOptions delayOptions) : base(delayOptions) { _delayOptions = delayOptions; if (delayOptions.UseJitter) { _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); - _getDelay = GetJitteredDelay; - } - else - { - _getDelay = GetBaseDelay; } } - public TimeSpan GetDelay(int attempt) => _getDelay(attempt); - - private TimeSpan GetBaseDelay(int attempt) + protected override TimeSpan GetBaseDelay(int attempt) { return _delayOptions.BaseDelay; } - private TimeSpan GetJitteredDelay(int attempt) + 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/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 . /// From 95e29750edd889903627fdece48ed0bb07bf742e Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 27 Mar 2026 10:50:11 +0300 Subject: [PATCH 105/109] Make `LinearDelayCore` inherit from `DelayCoreBase` and simplify `LinearRetryDelay` constructor initialization. --- src/Retry/LinearRetryDelay.cs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/Retry/LinearRetryDelay.cs b/src/Retry/LinearRetryDelay.cs index 0266f5ca..cef7dc9a 100644 --- a/src/Retry/LinearRetryDelay.cs +++ b/src/Retry/LinearRetryDelay.cs @@ -11,7 +11,7 @@ public sealed class LinearRetryDelay : RetryDelay /// Initializes a new instance of . /// /// - public LinearRetryDelay(LinearRetryDelayOptions retryDelayOptions): base(new LinearDelayCore(retryDelayOptions).GetDelay) + public LinearRetryDelay(LinearRetryDelayOptions retryDelayOptions): base(new LinearDelayCore(retryDelayOptions)) { #pragma warning disable CS0618 // Type or member is obsolete InnerDelay = this; @@ -55,36 +55,23 @@ public class LinearRetryDelayOptions : RetryDelayOptions public static implicit operator LinearRetryDelay(LinearRetryDelayOptions options) => new LinearRetryDelay(options); } - internal class LinearDelayCore + internal class LinearDelayCore : DelayCoreBase { private readonly LinearRetryDelayOptions _delayOptions; private readonly MaxDelayDelimiter _maxDelayDelimiter; - private readonly Func _getDelay; - - public LinearDelayCore(LinearRetryDelayOptions delayOptions) + public LinearDelayCore(LinearRetryDelayOptions delayOptions) : base(delayOptions) { _delayOptions = delayOptions; _maxDelayDelimiter = new MaxDelayDelimiter(delayOptions); - - if (delayOptions.UseJitter) - { - _getDelay = GetJitteredDelay; - } - else - { - _getDelay = GetBaseDelay; - } } - public TimeSpan GetDelay(int attempt) => _getDelay(attempt); - - private TimeSpan GetBaseDelay(int attempt) + protected override TimeSpan GetBaseDelay(int attempt) { return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(GetDelayValueInMs(attempt, _delayOptions)); } - private TimeSpan GetJitteredDelay(int attempt) + protected override TimeSpan GetJitteredDelay(int attempt) { return _maxDelayDelimiter.GetDelayLimitedToMaxDelayIfNeed(StandardJitter.AddJitter(GetDelayValueInMs(attempt, _delayOptions))); } From 40654ddde1cd30ecce08991e73ca9240432a27e9 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sat, 28 Mar 2026 16:08:11 +0300 Subject: [PATCH 106/109] Add `ExponentialDelayCore` and simplify `ExponentialRetryDelay` constructor initialization. --- src/Fallback/DefaultFallbackProcessor.cs | 1 - src/Retry/ExponentialRetryDelay.cs | 61 ++++++------ tests/RetryDelay.Tests/RetryDelayTests.cs | 116 ++++++++++++++++++++++ 3 files changed, 146 insertions(+), 32 deletions(-) diff --git a/src/Fallback/DefaultFallbackProcessor.cs b/src/Fallback/DefaultFallbackProcessor.cs index 716be9db..38ed969a 100644 --- a/src/Fallback/DefaultFallbackProcessor.cs +++ b/src/Fallback/DefaultFallbackProcessor.cs @@ -497,7 +497,6 @@ async Task policyRuleFunc(ErrorContext _, CancellationToken ct) return await HandleExceptionAsync(ex, result, emptyErrorContext, policyRuleFunc, configureAwait, token).ConfigureAwait(configureAwait); } - private Task HandleExceptionAsync( Exception ex, PolicyResult policyResult, diff --git a/src/Retry/ExponentialRetryDelay.cs b/src/Retry/ExponentialRetryDelay.cs index 26187522..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 . /// @@ -85,4 +56,32 @@ public class ExponentialRetryDelayOptions : RetryDelayOptions 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/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index bfa630d9..3c7f21de 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -427,6 +427,122 @@ public void Should_Create_LinearRetryDelay_Instance_With_Create_Method_Default_S Assert.That(result, Is.EqualTo(TimeSpan.FromMilliseconds(200))); } + [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) From f40d9ade98bc003ea247781a08541773abc4cee8 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 29 Mar 2026 20:34:35 +0300 Subject: [PATCH 107/109] Defer pipeline functions to future release in separate branch. --- src/FuncPipeline/PipelineFuncExtensions.cs | 27 --- src/FuncPipeline/PipelineResult.cs | 42 ---- tests/PipelineFuncExtensionsTests.cs | 50 ----- tests/PipelineResultTests.cs | 239 --------------------- tests/PoliNorError.Tests.csproj | 2 - 5 files changed, 360 deletions(-) delete mode 100644 src/FuncPipeline/PipelineFuncExtensions.cs delete mode 100644 src/FuncPipeline/PipelineResult.cs delete mode 100644 tests/PipelineFuncExtensionsTests.cs delete mode 100644 tests/PipelineResultTests.cs diff --git a/src/FuncPipeline/PipelineFuncExtensions.cs b/src/FuncPipeline/PipelineFuncExtensions.cs deleted file mode 100644 index 483f6c0f..00000000 --- a/src/FuncPipeline/PipelineFuncExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading; - -namespace PoliNorError -{ - internal static class PipelineFuncExtensions - { - internal static Func> ToHandledByPolicy(this Func func, SimplePolicy policy) - { - return (t, ct) => - { - var res = policy.Handle(func, t, ct); - - if (ct.IsCancellationRequested) - { - return PipelineResult.Failure(res, true); - } - - if (!res.NoError || res.IsCanceled) - { - return PipelineResult.Failure(res); - } - return PipelineResult.Success(res); - }; - } - } -} diff --git a/src/FuncPipeline/PipelineResult.cs b/src/FuncPipeline/PipelineResult.cs deleted file mode 100644 index 6fa844a0..00000000 --- a/src/FuncPipeline/PipelineResult.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace PoliNorError -{ - public sealed class PipelineResult - { - internal static PipelineResult Failure(PolicyResult failedPolicyResult) - { - return new PipelineResult() - { - FailedPolicyResult = failedPolicyResult, - IsCanceled = failedPolicyResult.IsCanceled - }; - } - - internal static PipelineResult Failure(PolicyResult failedPolicyResult, bool isCanceled) - { - return new PipelineResult() - { - FailedPolicyResult = failedPolicyResult, - IsCanceled = isCanceled - }; - } - - internal static PipelineResult Success(PolicyResult successPolicyResult) - { - return new PipelineResult() - { - SucceededPolicyResult = successPolicyResult, - }; - } - - private PipelineResult() { } - - internal PolicyResult FailedPolicyResult { get; private set; } - internal PolicyResult SucceededPolicyResult { get; private set; } - - public bool IsFailed => SucceededPolicyResult is null; - - public bool IsCanceled { get; private set; } - - public T Result => IsFailed ? default : SucceededPolicyResult.Result; - } -} diff --git a/tests/PipelineFuncExtensionsTests.cs b/tests/PipelineFuncExtensionsTests.cs deleted file mode 100644 index bf1d8390..00000000 --- a/tests/PipelineFuncExtensionsTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using NUnit.Framework; -using System; -using System.Threading; - -namespace PoliNorError.Tests -{ - public class PipelineFuncExtensionsTests - { - [Test] - public void Should_Return_Success_When_Policy_Handles_Successfully_And_No_Cancellation() - { - // Arrange - Func func = x => x.ToString(); - var policy = new SimplePolicy(); - var cancellationToken = CancellationToken.None; - - // Act - var wrappedFunc = func.ToHandledByPolicy(policy); - var result = wrappedFunc(42, cancellationToken); - - // Assert - Assert.That(result.IsFailed, Is.False); - Assert.That(result.IsCanceled, Is.False); - Assert.That(result.Result, Is.EqualTo("42")); - Assert.That(result.SucceededPolicyResult, Is.Not.Null); - } - - [Test] - public void Should_Return_Failure_When_CancellationToken_Is_Canceled() - { - // Arrange - Func func = x => x.ToString(); - var policy = new SimplePolicy(); - using (var cts = new CancellationTokenSource()) - { - cts.Cancel(); - - // Act - var wrappedFunc = func.ToHandledByPolicy(policy); - var result = wrappedFunc(42, cts.Token); - - // Assert - Assert.That(result.IsFailed, Is.True); - Assert.That(result.IsCanceled, Is.True); - Assert.That(result.FailedPolicyResult, Is.Not.Null); - Assert.That(result.SucceededPolicyResult, Is.Null); - } - } - } -} diff --git a/tests/PipelineResultTests.cs b/tests/PipelineResultTests.cs deleted file mode 100644 index cd242d57..00000000 --- a/tests/PipelineResultTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -using NUnit.Framework; - -namespace PoliNorError.Tests -{ - public class PipelineResultTests - { - [Test] - public void Should_Create_Failed_PipelineResult_With_PolicyResult() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailed(); - - // Act - var result = PipelineResult.Failure(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.True); - Assert.That(result.IsCanceled, Is.False); - Assert.That(result.Result, Is.EqualTo(default(int))); - } - - [Test] - public void Should_Create_Failed_PipelineResult_With_Canceled_PolicyResult() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailedAndCanceled(); - - // Act - var result = PipelineResult.Failure(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.True); - Assert.That(result.IsCanceled, Is.True); - Assert.That(result.Result, Is.Null); - } - - [Test] - public void Should_Create_Failed_PipelineResult_With_Explicit_Cancellation_Flag() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailed(); - - // Act - var result = PipelineResult.Failure(policyResult, isCanceled: true); - - // Assert - Assert.That(result.IsFailed, Is.True); - Assert.That(result.IsCanceled, Is.True); - Assert.That(result.Result, Is.EqualTo(default(double))); - } - - [Test] - public void Should_Create_Failed_PipelineResult_With_IsCanceled_False_When_Explicitly_Set() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailedAndCanceled(); - - // Act - var result = PipelineResult.Failure(policyResult, isCanceled: false); - - // Assert - Assert.That(result.IsFailed, Is.True); - Assert.That(result.IsCanceled, Is.False); - } - - [Test] - public void Should_Create_Success_PipelineResult_With_Result_Value() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetOk(); - policyResult.SetResult(42); - - // Act - var result = PipelineResult.Success(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.False); - Assert.That(result.IsCanceled, Is.False); - Assert.That(result.Result, Is.EqualTo(42)); - } - - [Test] - public void Should_Create_Success_PipelineResult_With_Reference_Type() - { - // Arrange - const string expectedValue = "test string"; - var policyResult = new PolicyResult(); - policyResult.SetOk(); - policyResult.SetResult(expectedValue); - - // Act - var result = PipelineResult.Success(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.False); - Assert.That(result.Result, Is.EqualTo(expectedValue)); - } - - [Test] - public void Should_Return_Default_Result_When_Failed() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailed(); - - // Act - var result = PipelineResult.Failure(policyResult); - - // Assert - Assert.That(result.Result, Is.EqualTo(0)); - } - - [Test] - public void Should_Return_Null_For_Reference_Type_When_Failed() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailed(); - - // Act - var result = PipelineResult.Failure(policyResult); - - // Assert - Assert.That(result.Result, Is.Null); - } - - [Test] - public void Should_Have_IsFailed_True_When_SucceededPolicyResult_Is_Null() - { - // Arrange - var policyResult = new PolicyResult(); - - // Act - var result = PipelineResult.Failure(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.True); - } - - [Test] - public void Should_Have_IsFailed_False_When_SucceededPolicyResult_Is_Not_Null() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetResult(true); - - // Act - var result = PipelineResult.Success(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.False); - } - - [Test] - public void Should_Preserve_IsCanceled_From_PolicyResult() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetCanceled(); - - // Act - var result = PipelineResult.Failure(policyResult); - - // Assert - Assert.That(result.IsCanceled, Is.True); - } - - [Test] - public void Should_Not_Set_IsCanceled_For_Success_Result() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetResult(100); - - // Act - var result = PipelineResult.Success(policyResult); - - // Assert - Assert.That(result.IsCanceled, Is.False); - } - - [Test] - public void Should_Handle_Nullable_Value_Type() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetResult(null); - - // Act - var result = PipelineResult.Success(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.False); - Assert.That(result.Result, Is.Null); - } - - [Test] - public void Should_Return_Correct_Result_For_Custom_Object() - { - // Arrange - var customObject = new TestClass { Value = "test" }; - var policyResult = new PolicyResult(); - policyResult.SetResult(customObject); - - // Act - var result = PipelineResult.Success(policyResult); - - // Assert - Assert.That(result.IsFailed, Is.False); - Assert.That(result.Result, Is.SameAs(customObject)); - Assert.That(result.Result.Value, Is.EqualTo("test")); - } - - [Test] - public void Should_Override_PolicyResult_IsCanceled_When_Explicitly_Provided() - { - // Arrange - var policyResult = new PolicyResult(); - policyResult.SetFailed(); - // Note: policyResult.IsCanceled is false - - // Act - var result = PipelineResult.Failure(policyResult, isCanceled: true); - - // Assert - Assert.That(result.IsCanceled, Is.True); - } - - private class TestClass - { - public string Value { get; set; } - } - } -} \ No newline at end of file diff --git a/tests/PoliNorError.Tests.csproj b/tests/PoliNorError.Tests.csproj index 8d60c5c5..46e2dcb8 100644 --- a/tests/PoliNorError.Tests.csproj +++ b/tests/PoliNorError.Tests.csproj @@ -115,8 +115,6 @@ - - From 1a6c325d92e967bb337a944ac419b4d053a7d5df Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 30 Mar 2026 13:17:26 +0300 Subject: [PATCH 108/109] Add `TimeSeriesDelayCore`, simplify `TimeSeriesRetryDelay` constructor initialization, and fix summary documentation. --- src/Retry/TimeSeriesRetryDelay.cs | 51 ++++++++------------ tests/RetryDelay.Tests/RetryDelayTests.cs | 58 +++++++++++++++++++++++ 2 files changed, 79 insertions(+), 30 deletions(-) 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/tests/RetryDelay.Tests/RetryDelayTests.cs b/tests/RetryDelay.Tests/RetryDelayTests.cs index 3c7f21de..883a80c7 100644 --- a/tests/RetryDelay.Tests/RetryDelayTests.cs +++ b/tests/RetryDelay.Tests/RetryDelayTests.cs @@ -427,6 +427,64 @@ public void Should_Create_LinearRetryDelay_Instance_With_Create_Method_Default_S 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 { From 805719a947937cf358402f71f51329e51202471d Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 31 Mar 2026 13:06:54 +0300 Subject: [PATCH 109/109] Add `propagateCancellation` parameter (default: `true`) to internal `FuncExtensions.ToTaskReturnFunc` overloads. --- src/Utilities/FuncEntensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Utilities/FuncEntensions.cs b/src/Utilities/FuncEntensions.cs index 972642f7..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); }