< Summary

Information
Class: Elsa.Workflows.Middleware.Activities.ExceptionHandlingMiddlewareExtensions
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 94
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
UseExceptionHandling(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs

#LineLine coverage
 1using Elsa.Extensions;
 2using Elsa.Workflows.Pipelines.ActivityExecution;
 3using Elsa.Workflows.Signals;
 4using Microsoft.Extensions.Logging;
 5
 6namespace Elsa.Workflows.Middleware.Activities;
 7
 8/// <summary>
 9/// Adds extension methods to <see cref="ExceptionHandlingMiddleware"/>.
 10/// </summary>
 11public static class ExceptionHandlingMiddlewareExtensions
 12{
 13    /// <summary>
 14    /// Installs the <see cref="ExceptionHandlingMiddleware"/> component in the activity execution pipeline.
 15    /// </summary>
 49416    public static IActivityExecutionPipelineBuilder UseExceptionHandling(this IActivityExecutionPipelineBuilder pipeline
 17}
 18
 19/// <summary>
 20/// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state,
 21/// unless an enclosing container claims the fault by handling the <see cref="FaultSignal"/>.
 22/// </summary>
 23public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncidentStrategyResolver incidentStrategyReso
 24    : IActivityExecutionMiddleware
 25{
 26    /// <inheritdoc />
 27    public async ValueTask InvokeAsync(ActivityExecutionContext context)
 28    {
 29        try
 30        {
 31            await next(context);
 32        }
 33        catch (Exception e)
 34        {
 35            logger.LogWarning(e, "An exception was caught from a downstream middleware component");
 36            context.Fault(e);
 37
 38            // Give the ancestor chain a chance to handle the fault. See FaultSignal for the contract: the handler owns
 39            // terminalizing the faulted activity, and recovering the fault bookkeeping is ours alone to do, exactly onc
 40            if (await TryHandOffToAncestorsAsync(context, e))
 41            {
 42                context.RecoverFromFault();
 43                return;
 44            }
 45
 46            await HandleIncidentAsync(context);
 47        }
 48    }
 49
 50    /// <summary>
 51    /// Offers the fault to the ancestor chain, and reports whether one of them claimed it.
 52    /// </summary>
 53    /// <remarks>
 54    /// A handler that throws is treated as not having handled the fault, so the incident strategy runs exactly as it
 55    /// would if no handler existed. This is the one place where swallowing an exception is right: the send happens
 56    /// inside the <c>catch</c> whose entire job is to stop exceptions escaping the activity pipeline, so letting a
 57    /// handler's failure through would defeat the middleware and lose the original fault along with it. Falling through
 58    /// to the incident strategy is also the conservative direction: a handler that failed part way through may have lef
 59    /// the faulted activity in any state, and surfacing that as an incident is better than reporting success. Both
 60    /// exceptions are logged, the handler's at error level, because a broken fault handler is a defect in its own right
 61    /// rather than a workflow outcome.
 62    /// <para>
 63    /// Cancellation is deliberately excluded. An <see cref="OperationCanceledException"/> from a handler means the host
 64    /// is tearing the run down, not that the handler is broken, and this repository consistently keeps the two apart:
 65    /// the workflow-level exception middleware cancels and rethrows before its general catch, and
 66    /// <c>WorkflowRunner</c> declines to record cancellation as the workflow's exception. Swallowing it here would
 67    /// convert a deliberate cancellation into a faulted workflow.
 68    /// </para>
 69    /// </remarks>
 70    private async Task<bool> TryHandOffToAncestorsAsync(ActivityExecutionContext context, Exception fault)
 71    {
 72        try
 73        {
 74            return await context.TrySendSignalAsync(new FaultSignal(fault, context));
 75        }
 76        catch (Exception handlerException) when (handlerException is not OperationCanceledException)
 77        {
 78            logger.LogError(
 79                handlerException,
 80                "A {SignalType} handler threw while an ancestor of activity {ActivityId} of type {ActivityType} was bein
 81                nameof(FaultSignal),
 82                context.Activity.Id,
 83                context.Activity.Type);
 84
 85            return false;
 86        }
 87    }
 88
 89    private async Task HandleIncidentAsync(ActivityExecutionContext context)
 90    {
 91        var strategy = await incidentStrategyResolver.ResolveStrategyAsync(context);
 92        strategy.HandleIncident(context);
 93    }
 94}