diff --git a/Directory.Packages.props b/Directory.Packages.props index 828ac6d..b405535 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,6 +26,7 @@ + @@ -71,4 +72,4 @@ - \ No newline at end of file + diff --git a/src/QuickApiMapper.Application/Core/BehaviorPipeline.cs b/src/QuickApiMapper.Application/Core/BehaviorPipeline.cs index 2f79806..4252d7f 100644 --- a/src/QuickApiMapper.Application/Core/BehaviorPipeline.cs +++ b/src/QuickApiMapper.Application/Core/BehaviorPipeline.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using PatternKit.Behavioral.Chain; using QuickApiMapper.Contracts; using ContractsMappingResult = QuickApiMapper.Contracts.MappingResult; @@ -24,163 +25,167 @@ public async Task ExecuteAsync( MappingContext context, Func> coreLogic) { - // Build the complete pipeline: PreRun -> WholeRun -> Core -> PostRun - var pipeline = BuildCompletePipeline(coreLogic); - - // Execute the complete pipeline (exceptions from PreRun behaviors will propagate) - var result = await pipeline(context); + await ExecutePreRunBehaviors(context); + var result = await ExecuteWholeRunWithPostRunBehaviors(context, coreLogic); logger.LogInformation("Behavior pipeline execution completed. Success: {Success}", result.IsSuccess); return result; } - /// - /// Builds the complete pipeline: PreRun -> WholeRun -> Core -> PostRun - /// - private Func> BuildCompletePipeline( + private async Task ExecuteWholeRunWithPostRunBehaviors( + MappingContext context, Func> coreLogic) { - - var wholeRunPipeline = BuildWholeRunPipeline(coreLogic); - var wholeRunWithPost = BuildCoreWithPostRun(wholeRunPipeline); + try + { + var result = await ExecuteWholeRunBehaviors(context, coreLogic); - return BuildPreRunPipeline(wholeRunWithPost); - } + await ExecutePostRunBehaviors(context, result); - /// - /// Builds the PreRun behavior pipeline chain. - /// - private Func> BuildPreRunPipeline( - Func> next) - { - return async context => + return result; + } + catch (Exception ex) when (IsNonFatalPipelineException(ex)) { - // Execute PreRun behaviors first (let exceptions propagate for fail-fast scenarios) - await ExecutePreRunBehaviors(context); + var failureResult = ContractsMappingResult.Failure("Core mapping logic failed", ex); - // Then execute the rest of the pipeline - return await next(context); - }; + try + { + await ExecutePostRunBehaviors(context, failureResult); + } + catch (Exception postRunEx) when (IsNonFatalPipelineException(postRunEx)) + { + logger.LogError(postRunEx, "PostRun behavior execution failed after core logic failure"); + } + + return failureResult; + } } - /// - /// Builds the WholeRun behavior pipeline chain. - /// - private Func> BuildWholeRunPipeline( + private async Task ExecuteWholeRunBehaviors( + MappingContext context, Func> coreLogic) { - // Get ordered WholeRun behaviors - var orderedBehaviors = wholeRunBehaviors - .OrderBy(b => b.Order) - .ToList(); - - // Build the pipeline from right to left (last behavior wraps the core logic) - var pipeline = coreLogic; + var state = new BehaviorExecutionState(context, coreLogic); + var builder = AsyncActionChain.Create(); - // Wrap with WholeRun behaviors in reverse order - for (var i = orderedBehaviors.Count - 1; i >= 0; i--) + foreach (var behavior in wholeRunBehaviors.OrderBy(b => b.Order)) { - var behavior = orderedBehaviors[i]; - var next = pipeline; // Capture the current pipeline - - pipeline = async context => + builder.Use(async (current, ct, next) => { logger.LogDebug("Executing WholeRun behavior: {BehaviorName}", behavior.Name); - return await behavior.ExecuteAsync(context, next); - }; + current.Result = await behavior.ExecuteAsync(current.Context, ContinueAsync).ConfigureAwait(false); + + async Task ContinueAsync(MappingContext nextContext) + { + var previousContext = current.Context; + current.Context = nextContext; + + try + { + await next(current, ct).ConfigureAwait(false); + return current.Result ?? CreateMissingResultFailure(); + } + finally + { + current.Context = previousContext; + } + } + }); } - return pipeline; + builder.Finally(async (current, _) => + { + current.Result = await current.CoreLogic(current.Context).ConfigureAwait(false); + }); + + await builder.Build().ExecuteAsync(state, context.CancellationToken).ConfigureAwait(false); + + return state.Result ?? CreateMissingResultFailure(); } - /// - /// Builds the core logic wrapped with PostRun behaviors. - /// - private Func> BuildCoreWithPostRun( - Func> coreLogic) + private async Task ExecutePreRunBehaviors(MappingContext context) { - return async context => - { - try - { - // Execute core logic - var result = await coreLogic(context); - - // Execute PostRun behaviors - await ExecutePostRunBehaviors(context, result); + var builder = AsyncActionChain.Create(); - return result; - } - catch (Exception ex) + foreach (var behavior in preRunBehaviors.OrderBy(b => b.Order)) + { + builder.Use(async (current, ct, next) => { - var failureResult = ContractsMappingResult.Failure("Core mapping logic failed", ex); + logger.LogDebug("Executing PreRun behavior: {BehaviorName}", behavior.Name); - // Still try to execute PostRun behaviors even if core logic failed try { - await ExecutePostRunBehaviors(context, failureResult); + await behavior.ExecuteAsync(current).ConfigureAwait(false); + logger.LogDebug("PreRun behavior completed successfully: {BehaviorName}", behavior.Name); } - catch (Exception postRunEx) + catch (Exception ex) when (IsNonFatalPipelineException(ex)) { - logger.LogError(postRunEx, "PostRun behavior execution failed after core logic failure"); + logger.LogError(ex, "PreRun behavior failed: {BehaviorName}", behavior.Name); + throw; } - return failureResult; - } - }; - } + await next(current, ct).ConfigureAwait(false); + }); + } + await builder.Build().ExecuteAsync(context, context.CancellationToken).ConfigureAwait(false); + } - /// - /// Executes all PreRun behaviors in order. - /// - private async Task ExecutePreRunBehaviors(MappingContext context) + private async Task ExecutePostRunBehaviors( + MappingContext context, + ContractsMappingResult result) { - var orderedBehaviors = preRunBehaviors - .OrderBy(b => b.Order) - .ToList(); + var state = new PostRunBehaviorExecutionState(context, result); + var builder = AsyncActionChain.Create(); - foreach (var behavior in orderedBehaviors) + foreach (var behavior in postRunBehaviors.OrderBy(b => b.Order)) { - logger.LogDebug("Executing PreRun behavior: {BehaviorName}", behavior.Name); - - try - { - await behavior.ExecuteAsync(context); - logger.LogDebug("PreRun behavior completed successfully: {BehaviorName}", behavior.Name); - } - catch (Exception ex) + builder.Use(async (current, ct, next) => { - logger.LogError(ex, "PreRun behavior failed: {BehaviorName}", behavior.Name); - throw; - } + logger.LogDebug("Executing PostRun behavior: {BehaviorName}", behavior.Name); + + try + { + await behavior.ExecuteAsync(current.Context, current.Result).ConfigureAwait(false); + logger.LogDebug("PostRun behavior completed successfully: {BehaviorName}", behavior.Name); + } + catch (Exception ex) when (IsNonFatalPipelineException(ex)) + { + logger.LogError(ex, "PostRun behavior failed: {BehaviorName}", behavior.Name); + } + + await next(current, ct).ConfigureAwait(false); + }); } + + await builder.Build().ExecuteAsync(state, context.CancellationToken).ConfigureAwait(false); } - /// - /// Executes all PostRun behaviors in order. - /// - private async Task ExecutePostRunBehaviors( - MappingContext context, - ContractsMappingResult result) - { - var orderedBehaviors = postRunBehaviors - .OrderBy(b => b.Order) - .ToList(); + private static ContractsMappingResult CreateMissingResultFailure() + => ContractsMappingResult.Failure("Behavior pipeline completed without producing a mapping result"); - foreach (var behavior in orderedBehaviors) - { - logger.LogDebug("Executing PostRun behavior: {BehaviorName}", behavior.Name); + private static bool IsNonFatalPipelineException(Exception exception) + => exception is not OperationCanceledException + and not OutOfMemoryException + and not StackOverflowException + and not AccessViolationException + and not AppDomainUnloadedException + and not BadImageFormatException; - try - { - await behavior.ExecuteAsync(context, result); - logger.LogDebug("PostRun behavior completed successfully: {BehaviorName}", behavior.Name); - } - catch (Exception ex) - { - logger.LogError(ex, "PostRun behavior failed: {BehaviorName}", behavior.Name); - } - } + private sealed class BehaviorExecutionState( + MappingContext context, + Func> coreLogic) + { + public MappingContext Context { get; set; } = context; + public Func> CoreLogic { get; } = coreLogic; + public ContractsMappingResult? Result { get; set; } + } + + private sealed class PostRunBehaviorExecutionState( + MappingContext context, + ContractsMappingResult result) + { + public MappingContext Context { get; } = context; + public ContractsMappingResult Result { get; } = result; } -} \ No newline at end of file +} diff --git a/src/QuickApiMapper.Application/QuickApiMapper.Application.csproj b/src/QuickApiMapper.Application/QuickApiMapper.Application.csproj index 5c3bd89..7dc78ae 100644 --- a/src/QuickApiMapper.Application/QuickApiMapper.Application.csproj +++ b/src/QuickApiMapper.Application/QuickApiMapper.Application.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/QuickApiMapper.UnitTests/BehaviorIntegrationTests.cs b/tests/QuickApiMapper.UnitTests/BehaviorIntegrationTests.cs index 0cf5b35..1b9a6f1 100644 --- a/tests/QuickApiMapper.UnitTests/BehaviorIntegrationTests.cs +++ b/tests/QuickApiMapper.UnitTests/BehaviorIntegrationTests.cs @@ -438,6 +438,36 @@ public async Task BehaviorTestCollection_WithFactoryMethods_ShouldBuildCorrectPi }); } + [Test] + public async Task Pipeline_WithShortCircuitingWholeRunBehavior_ShouldStillExecutePostRun() + { + // Arrange + var executionOrder = new List(); + var postRunBehavior = new TestPostRunBehavior(executionOrder); + var shortCircuitBehavior = new TestShortCircuitWholeRunBehavior(executionOrder); + + var pipeline = _behaviorCollection + .AddWholeRunBehavior(shortCircuitBehavior) + .AddPostRunBehavior(postRunBehavior) + .BuildPipeline(_serviceProvider); + + var context = CreateMappingContext(); + + // Act + var result = await pipeline.ExecuteAsync(context, _ => + { + executionOrder.Add("Core"); + return Task.FromResult(MappingResult.Success()); + }); + + Assert.Multiple(() => + { + // Assert + Assert.That(result.IsSuccess, Is.True); + Assert.That(executionOrder, Is.EqualTo(new[] { "ShortCircuit", "PostRun" })); + }); + } + private class TestPreRunBehavior( List executionOrder ) : @@ -490,6 +520,21 @@ public async Task ExecuteAsync(MappingContext context, Func executionOrder + ) : + IWholeRunBehavior + { + public string Name => "TestShortCircuit"; + public int Order => 10; + + public Task ExecuteAsync(MappingContext context, Func> next) + { + executionOrder.Add("ShortCircuit"); + return Task.FromResult(MappingResult.Success()); + } + } + #pragma warning disable IDISP013 #pragma warning disable IDISP014 @@ -551,4 +596,4 @@ public void Dispose() _httpClient.Dispose(); _httpClientFactory.Dispose(); } -} \ No newline at end of file +}