Skip to content

fix: Refactored the default activity invoker to handle exceptions#7426

Open
ByronMayne wants to merge 2 commits into
elsa-workflows:mainfrom
ByronMayne:fix/refactored-activity-invocation-exception-handling
Open

fix: Refactored the default activity invoker to handle exceptions#7426
ByronMayne wants to merge 2 commits into
elsa-workflows:mainfrom
ByronMayne:fix/refactored-activity-invocation-exception-handling

Conversation

@ByronMayne
Copy link
Copy Markdown
Contributor

Fixes: #7372

Refactored the DefaultActivityInvokerMiddleware to handle exceptions at the call site to make sure that the behaviour of the middleware remains consisten .

Purpose

Exceptions were only handled at the middleware level and the behaviour would change when faults happened depending on if the fault was created by throwing an exception or using the Fault method. This change makes the middleware handle exceptions and fault in a consistent manor

Scope

Select one primary concern:

  • [X ] Bug fix (behavior change)
  • Refactor (no behavior change)
  • Documentation update
  • Formatting / code cleanup
  • Dependency / build update
  • New feature

Description

Problem

The behaviour of Activity execution event would change depending on how faults are created

Solution

Exceptions are handled at the invocation level instead of depending on the middleware.


Verification

Run the unit tests which create an real pipeline and validate the results. They would fail before the fix was created.


Screenshots / Recordings (if applicable)

Checklist

  • The PR is focused on a single concern
  • Commit messages follow the recommended convention
  • [ X] Tests added or updated (if applicable)
  • Documentation updated (if applicable)
  • No unrelated cleanup included
  • All tests pass

Before there was no exception handling so the behaviour of the activity execution middleware would change depending on if a fault was created by throwing or by using the Fault method.
Copilot AI review requested due to automatic review settings April 26, 2026 20:42
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR refactors DefaultActivityInvokerMiddleware to catch exceptions from both CanExecuteAsync and ExecuteActivityAsync at the call site, ensuring fault handling (incident creation, status transition, strategy invocation) is consistent regardless of whether a fault originates from a thrown exception or a direct context.Fault() call. A secondary defensive fix in ActivityDescriber.GetFriendlyActivityName guards against IndexOf('')returning-1` for edge-case generic types. New unit tests cover all four paths: execute-throws, execute-faults, can-execute-throws, and notification publishing.

Confidence Score: 5/5

Safe to merge — the core fault-handling change is well-scoped and backed by new unit tests; only minor P2 test quality issues remain.

All findings are P2 (style/completeness): an unused import and a missing AggregateFaultCount assertion in one test method. No logic bugs or regressions were identified in the production code path.

ActivityInvokerMiddlewareTestsBase.cs — unused import and incomplete assertion on the CanExecuteThrows fault path.

Important Files Changed

Filename Overview
src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs Adds try/catch around both CanExecuteAsync and ExecuteActivityAsync; exceptions now fault the activity at the call site consistently with direct context.Fault() usage.
src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs Guards GetFriendlyActivityName against IndexOf returning -1 for edge-case generic types; defensive fix, no behavioural regressions.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityExecutionMiddlewareTestsBase.cs New base test fixture with TestActivity supporting throw/fault/cancel scenarios; good overall, some test property name typos flagged in prior review threads.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityInvokerMiddlewareTestsBase.cs New abstract test base with coverage for exception-throw and direct-fault paths; unused System.ComponentModel import and missing AggregateFaultCount assertion on CanExecute path.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/DefaultActivityInvokerMiddlewareTests.cs Minimal concrete test class wiring DefaultActivityInvokerMiddleware into the base test suite; looks correct.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/NotificationPublishingMiddlewareTests.cs Tests that ActivityExecuting/ActivityExecuted notifications are raised; pipeline ordering (NotificationPublishingMiddleware → DefaultActivityInvokerMiddleware) is correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[InvokeAsync] --> B[EvaluateInputProperties]
    B --> C{Cancellation Requested?}
    C -- Yes --> D[TransitionTo Canceled / return]
    C -- No --> E[CanExecuteAsync]
    E -- throws OperationCanceled --> F[CancelActivityAsync / return]
    E -- throws Exception --> G[Fault + HandleIncident / return]
    E -- returns false --> H[TransitionTo Pending / return]
    E -- returns true --> I[EnterExecution / TransitionTo Running]
    I --> J[ExecuteActivityAsync]
    J -- throws OperationCanceled --> K[CancelActivityAsync / return]
    J -- throws Exception --> L[Fault + HandleIncident / return]
    J -- returns normally --> M[Reset ExecuteDelegate / Auto-burn bookmark / IncrementExecutionCount]
    M --> N[next context]
    N --> O{activityDidComplete?}
    O -- Yes --> P[Send ActivityCompleted]
    O -- No --> Q[ShouldCommit check / done]
    P --> Q
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityInvokerMiddlewareTestsBase.cs
Line: 1-4

Comment:
**Unused import after removing `[Description]`**

`System.ComponentModel` is imported but nothing in this file uses it. If the `[Description]` attribute that prompted this import was intentionally removed, the `using` directive should be cleaned up too.

```suggestion
using Elsa.Mediator.Contracts;
using NSubstitute;
using Xunit.Abstractions;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityInvokerMiddlewareTestsBase.cs
Line: 83-84

Comment:
**Missing `AggregateFaultCount` assertion in `CanExecute` fault test**

The parallel `ActivityExceuteThrows_ActivityStatus_IsFaulted` test asserts both `Status == Faulted` and `AggregateFaultCount == 1`, but this test only checks `Status`. Given the PR's goal is consistent fault handling across all execution paths, the `CanExecuteAsync` exception path should be verified to produce the same fault count.

```suggestion
        Assert.Equal(ActivityStatus.Faulted, ExecutionContext.Status);
        Assert.Equal(1, ExecutionContext.AggregateFaultCount);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "Did some changes based off the code revi..." | Re-trigger Greptile

Comment on lines +26 to +29
public Exception? ExectueThrows { get; set; }
public Exception? ExectueFaults { get; set; }
public Exception? CanExecuteThrows { get; set; }
public bool AutoComplete { get; set; } = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Typos in test property names

ExectueThrows, ExectueFaults, and ExectueThrows are misspellings of ExecuteThrows and ExecuteFaults. These names appear in test assertion messages and test setup code, making typo-induced bugs harder to spot.

Suggested change
public Exception? ExectueThrows { get; set; }
public Exception? ExectueFaults { get; set; }
public Exception? CanExecuteThrows { get; set; }
public bool AutoComplete { get; set; } = false;
public Exception? ExecuteThrows { get; set; }
public Exception? ExecuteFaults { get; set; }
public Exception? CanExecuteThrows { get; set; }
public bool AutoComplete { get; set; } = false;
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityExecutionMiddlewareTestsBase.cs
Line: 26-29

Comment:
**Typos in test property names**

`ExectueThrows`, `ExectueFaults`, and `ExectueThrows` are misspellings of `ExecuteThrows` and `ExecuteFaults`. These names appear in test assertion messages and test setup code, making typo-induced bugs harder to spot.

```suggestion
        public Exception? ExecuteThrows { get; set; }
        public Exception? ExecuteFaults { get; set; }
        public Exception? CanExecuteThrows { get; set; }
        public bool AutoComplete { get; set; } = false;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +33 to +47
[Fact]
[Description("There was a bug where DefaultActivityInvoker was raising the ")]
public async Task ActivityExectue_RaiseNoEvents()
{
// Setup, force auto complete since that triggers the event
_activity.AutoComplete = true;

// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
await _notificationSender
.DidNotReceive()
.SendAsync(Arg.Any<INotification>());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Incomplete [Description] and overly-broad assertion

The [Description] text is cut off mid-sentence. More importantly, the assertion DidNotReceive().SendAsync(Arg.Any<INotification>()) covers all notifications. DefaultActivityInvokerMiddleware sends an ActivityCompleted notification whenever activityDidComplete is true, which is the case here with AutoComplete = true. If the mock matches that call (same CancellationToken path), this test will fail. If it doesn't match (e.g. due to a CancellationToken value mismatch), the test passes vacuously without verifying the intended constraint. The description should be completed and the assertion scoped to the specific event types that shouldn't be raised by this middleware (e.g. ActivityExecuting / ActivityExecuted).

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityInvokerMiddlewareTestsBase.cs
Line: 33-47

Comment:
**Incomplete `[Description]` and overly-broad assertion**

The `[Description]` text is cut off mid-sentence. More importantly, the assertion `DidNotReceive().SendAsync(Arg.Any<INotification>())` covers *all* notifications. `DefaultActivityInvokerMiddleware` sends an `ActivityCompleted` notification whenever `activityDidComplete` is `true`, which is the case here with `AutoComplete = true`. If the mock matches that call (same `CancellationToken` path), this test will fail. If it doesn't match (e.g. due to a `CancellationToken` value mismatch), the test passes vacuously without verifying the intended constraint. The description should be completed and the assertion scoped to the specific event types that shouldn't be raised by this middleware (e.g. `ActivityExecuting` / `ActivityExecuted`).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +20 to +87
[Fact]
public async Task ActivityExceuteThrows_IncidentsCount_IsOne()
{
// Setup
_activity.ExectueThrows = new Exception("EXCEPTION!");

// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
Assert.Single(ExecutionContext.WorkflowExecutionContext.Incidents);
}

[Fact]
[Description("There was a bug where DefaultActivityInvoker was raising the ")]
public async Task ActivityExectue_RaiseNoEvents()
{
// Setup, force auto complete since that triggers the event
_activity.AutoComplete = true;

// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
await _notificationSender
.DidNotReceive()
.SendAsync(Arg.Any<INotification>());
}

[Fact]
public async Task ActivityExceuteThrows_ActivityStatus_IsFaulted()
{
// Setup
_activity.ExectueThrows = new Exception("EXCEPTION!");

// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
Assert.Equal(ActivityStatus.Faulted, ExecutionContext.Status);
}

[Fact]
public async Task ActivityCanExceuteThrows_IncidentsCount_IsOne()
{
// Setup
_activity.CanExecuteThrows = new Exception("EXCEPTION!");

// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
Assert.Single(ExecutionContext.WorkflowExecutionContext.Incidents);
}

[Fact]
public async Task ActivityCanExceuteThrows_ActivityStatus_IsFaulted()
{
// Setup
_activity.CanExecuteThrows = new Exception("EXCEPTION!");

// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
Assert.Equal(ActivityStatus.Faulted, ExecutionContext.Status);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No test coverage for ExecuteFaults (direct-fault) path

TestActivity exposes an ExectueFaults property (which calls context.Fault() directly without throwing), but ActivityInvokerMiddlewareTestsBase only tests the exception-throwing paths. The PR's stated goal is to make the exception-thrown and direct-fault paths consistent, yet there is no test that verifies the incident count and status are correct when ExectueFaults is set. Without this, a regression on the fault path would go undetected.

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityInvokerMiddlewareTestsBase.cs
Line: 20-87

Comment:
**No test coverage for `ExecuteFaults` (direct-fault) path**

`TestActivity` exposes an `ExectueFaults` property (which calls `context.Fault()` directly without throwing), but `ActivityInvokerMiddlewareTestsBase` only tests the exception-throwing paths. The PR's stated goal is to make the exception-thrown and direct-fault paths *consistent*, yet there is no test that verifies the incident count and status are correct when `ExectueFaults` is set. Without this, a regression on the fault path would go undetected.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +18 to +33
{
// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
await _notificationSender.Received().SendAsync(Arg.Any<ActivityExecuting>());
}

[Fact]
public async Task InvokeAsync_Rasies_ActivityExecutedEvent()
{
// Act
await Pipeline.ExecuteAsync(ExecutionContext);

// Assert
await _notificationSender.Received().SendAsync(Arg.Any<ActivityExecuted>());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Typos in test method names

InvokeAsync_Rasies_ActivityExecutingEvent and InvokeAsync_Rasies_ActivityExecutedEvent contain the misspelling "Rasies" (should be "Raises"). While this doesn't affect test execution, it makes the test suite harder to read.

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/NotificationPublishingMiddlewareTests.cs
Line: 18-33

Comment:
**Typos in test method names**

`InvokeAsync_Rasies_ActivityExecutingEvent` and `InvokeAsync_Rasies_ActivityExecutedEvent` contain the misspelling "Rasies" (should be "Raises"). While this doesn't affect test execution, it makes the test suite harder to read.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses inconsistent activity lifecycle notifications when faults are produced via throw vs context.Fault(...) by handling activity exceptions inside DefaultActivityInvokerMiddleware (so upstream middleware like NotificationPublishingMiddleware can still complete its post-invocation behavior).

Changes:

  • Catch and convert exceptions from CanExecuteAsync and activity execution into faults/incidents inside DefaultActivityInvokerMiddleware.
  • Add unit test infrastructure and initial tests for activity execution middleware behavior and notification publishing.
  • Harden ActivityDescriber generic type friendly-name formatting to avoid substring issues.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs Handles exceptions at the invoker call site and routes them through incident handling instead of letting them bubble past notification middleware.
src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs Makes generic type name formatting safer by guarding the backtick index lookup.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityExecutionMiddlewareTestsBase.cs Introduces a reusable pipeline-based test fixture for activity execution middleware.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/ActivityInvokerMiddlewareTestsBase.cs Adds a reusable test suite for activity-invoker middleware behavior (faulting and incidents).
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/DefaultActivityInvokerMiddlewareTests.cs Hooks the standard invoker tests up to DefaultActivityInvokerMiddleware.
test/unit/Elsa.Workflows.Core.UnitTests/Middleware/Activities/NotificationPublishingMiddlewareTests.cs Adds basic tests asserting ActivityExecuting/ActivityExecuted notifications are sent.

Comment on lines +26 to +29
public Exception? ExectueThrows { get; set; }
public Exception? ExectueFaults { get; set; }
public Exception? CanExecuteThrows { get; set; }
public bool AutoComplete { get; set; } = false;
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several newly introduced identifiers appear to be misspelled (e.g., ExectueThrows / ExectueFaults). Since these are part of the shared test fixture API, consider renaming them to ExecuteThrows / ExecuteFaults to improve readability and avoid propagating typos into new tests.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When an activity has a fault depending on how it's created there a different events raised

2 participants