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
+}