diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 6d57db0ee..41f39a82b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -51,7 +51,7 @@ docker/ # Docker configurations
## Build Instructions
### Prerequisites
-- **.NET 9.0 SDK** (verified working version: 9.0.305)
+- **.NET 10.0 SDK**
- **Build time**: Initial restore ~1-2 minutes, full compile ~5-10 minutes
### Critical Build Information
@@ -111,11 +111,10 @@ find test/unit -name "*.csproj" | head -5 | xargs -I {} dotnet build {}
### Expected Build Warnings
- `NU1900`: Unable to load service index for external feeds (safe to ignore)
- `NU1801`: Service index warnings for feedz.io sources (safe to ignore)
-- `NU1101`: Missing Elsa.Studio packages (blocks studio app builds)
### Successful Build Indicators
- Core modules (Elsa.Workflows.Core, etc.) compile successfully
-- Server applications (Elsa.Server.Web) build without the studio UI
+- Server applications (Elsa.Server.Web) build
- Most modules show "succeeded with X warning(s)" (warnings are acceptable)
## Testing
@@ -142,7 +141,7 @@ dotnet test --no-build --no-restore [project-path]
### Code Standards
- **Language version**: C# latest
-- **Target framework**: .NET 9.0
+- **Target framework**: .NET 10.0
- **Nullable reference types**: Enabled
- **Implicit usings**: Enabled
- **EditorConfig**: Configured (4-space indentation, CRLF line endings)
@@ -154,23 +153,22 @@ dotnet test --no-build --no-restore [project-path]
- **Async/await**: Extensive use throughout for scalability
### Common Gotchas
-1. **External Dependencies**: Studio-related projects require external packages
-2. **NuGet Source Mapping**: Configured in NuGet.Config, restricts where packages can be sourced
-3. **Multiple Target Frameworks**: Some projects conditionally target different frameworks
-4. **Build Warnings**: Many NU1900/NU1801 warnings are expected and safe
+1. **NuGet Source Mapping**: Configured in NuGet.Config, restricts where packages can be sourced
+2**Multiple Target Frameworks**: Some projects conditionally target different frameworks
+3**Build Warnings**: Many NU1900/NU1801 warnings are expected and safe
## Continuous Integration
### GitHub Actions Workflow
- **Trigger**: Pull requests to `main` branch
- **Runner**: ubuntu-latest
-- **.NET Version**: 9.x (latest)
+- **.NET Version**: 10.x (latest)
- **Commands**: `./build.cmd Compile Test Pack`
- **File**: `.github/workflows/pr.yml` (auto-generated by NUKE)
### CI Pipeline Steps
1. Checkout code
-2. Setup .NET 9.x SDK
+2. Setup .NET 10.x SDK
3. Execute: Compile → Test → Pack
4. Expected warnings for external feed access
5. Studio apps may be excluded from CI builds
@@ -230,29 +228,6 @@ dotnet run
The server will start on the configured ports (check `appsettings.json` or environment variables).
-### Combined Server and Studio
-
-To run both the workflow server and visual studio together:
-
-```bash
-cd src/apps/Elsa.ServerAndStudio.Web
-dotnet restore --ignore-failed-sources
-dotnet run
-```
-
-Access the studio at the configured URL (typically `http://localhost:5000` or as configured).
-
-### Using Docker
-
-For quick testing with Docker (see README for full details):
-
-```bash
-docker pull elsaworkflows/elsa-server-and-studio-v3:latest
-docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e HTTP__BASEURL=http://localhost:13000 -p 13000:8080 elsaworkflows/elsa-server-and-studio-v3:latest
-```
-
-Default credentials: username `admin`, password `password`
-
## Troubleshooting
### Common Issues
@@ -296,12 +271,11 @@ Default credentials: username `admin`, password `password`
## Important Notes for Coding Agents
1. **Always use `--ignore-failed-sources`** when restoring packages
-2. **Focus on core workflow functionality** rather than studio UI components
-3. **Studio apps require external packages** that may not be accessible
-4. **Build warnings are normal** - don't try to fix NU1900/NU1801 warnings
-5. **Test individual modules** rather than solution-wide tests when external deps fail
-6. **Use direct dotnet commands** for building specific components when NUKE fails
-7. **Check project references** before attempting builds - some projects have conditional references
-8. **Start with core modules** like `Elsa.Workflows.Core`, `Elsa.Workflows.Runtime` which are more likely to build successfully
+2. **Focus on core workflow functionality**
+3. **Build warnings are normal** - don't try to fix NU1900/NU1801 warnings
+4. **Test individual modules** rather than solution-wide tests when external deps fail
+5. **Use direct dotnet commands** for building specific components when NUKE fails
+6. **Check project references** before attempting builds - some projects have conditional references
+7. **Start with core modules** like `Elsa.Workflows.Core`, `Elsa.Workflows.Runtime` which are more likely to build successfully
Trust these instructions for build and development workflows. Only search for additional information if these instructions are incomplete or found to be incorrect.
\ No newline at end of file
diff --git a/.github/workflows/copilot-setup-steps.yml.yml b/.github/workflows/copilot-setup-steps.yml
similarity index 80%
rename from .github/workflows/copilot-setup-steps.yml.yml
rename to .github/workflows/copilot-setup-steps.yml
index f5f80a5d3..9070ba5c4 100644
--- a/.github/workflows/copilot-setup-steps.yml.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -13,7 +13,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- - name: Set up .NET 9 SDK
+ - name: Set up .NET 10 SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
diff --git a/.github/workflows/docker-ca.yml b/.github/workflows/docker-ca.yml
index e8a737d48..0a35f9c30 100644
--- a/.github/workflows/docker-ca.yml
+++ b/.github/workflows/docker-ca.yml
@@ -48,7 +48,7 @@ jobs:
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Publish TLS smoke test app
run: dotnet publish test/TlsSmoke/TlsSmoke.csproj -c Release -o artifacts/tls-smoke
diff --git a/.github/workflows/elsa-server-and-studio.yml b/.github/workflows/elsa-server-and-studio.yml
deleted file mode 100644
index 6df73dfd2..000000000
--- a/.github/workflows/elsa-server-and-studio.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-name: Elsa Server + Studio Docker image
-on:
- workflow_dispatch:
- push:
- branches:
- - develop/3.6.0
-
-jobs:
- push_to_registry:
- name: Push Docker image to Docker Hub
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v3
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
- with:
- platforms: all
-
- - name: Set up Docker Buildx
- id: buildx
- uses: docker/setup-buildx-action@v2
-
- - name: Docker meta
- id: meta
- uses: docker/metadata-action@v3
- with:
- # list of Docker images to use as base name for tags
- images: |
- elsaworkflows/elsa-server-and-studio-v3-6-preview
- flavor: |
- latest=true
- # generate Docker tags based on the following events/attributes
- tags: |
- type=sha
-
- - name: Login to DockerHub
- if: github.event_name != 'pull_request'
- uses: docker/login-action@v2
- with:
- username: ${{ secrets.DOCKER_USER }}
- password: ${{ secrets.DOCKER_PASS }}
-
- - name: Build and push
- id: docker_build
- uses: docker/build-push-action@v3
- with:
- builder: ${{ steps.buildx.outputs.name }}
- context: .
- file: ./docker/ElsaServerAndStudio.Dockerfile
- platforms: linux/amd64,linux/arm64,linux/arm/v7
- push: ${{ github.event_name != 'pull_request' }}
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=local,src=/tmp/.buildx-cache
- cache-to: type=local,dest=/tmp/.buildx-cache
-
- - name: Image digest
- run: echo ${{ steps.docker_build.outputs.digest }}
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 8fac46a64..c3c434b40 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -37,7 +37,7 @@ jobs:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
- 9.x
+ 10.x
- uses: actions/checkout@v4
- name: 'Run: Compile, Test'
run: ./build.cmd Compile Test
diff --git a/Elsa.sln b/Elsa.sln
index 66d805dc4..2d7a9e618 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -94,17 +94,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Core.UnitTes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Tenants", "src\modules\Elsa.Tenants\Elsa.Tenants.csproj", "{29638A67-E79F-44FE-AC05-DA499EBA929E}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{B789B743-0FBE-4BBC-9F45-84C20C8D4814}"
- ProjectSection(SolutionItems) = preProject
- .github\workflows\bounty.yml = .github\workflows\bounty.yml
- .github\workflows\elsa-server-and-studio.yml = .github\workflows\elsa-server-and-studio.yml
- .github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
- .github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
- .github\workflows\packages.yml = .github\workflows\packages.yml
- .github\workflows\pr.yml = .github\workflows\pr.yml
- .github\workflows\stale.yml = .github\workflows\stale.yml
- EndProjectSection
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C80C8231-D35C-4ACC-9ED6-9F3DB221535E}"
@@ -313,6 +302,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scheduling.UnitTests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Common.IntegrationTests", "test\integration\Elsa.Common.IntegrationTests\Elsa.Common.IntegrationTests.csproj", "{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{06DC6553-AAEF-4FC8-B16F-039F292E86A3}"
+ ProjectSection(SolutionItems) = preProject
+ .github\copilot-instructions.md = .github\copilot-instructions.md
+ .github\dependabot.yml = .github\dependabot.yml
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{2158FC82-6253-47F2-9216-B266710D50F1}"
+ ProjectSection(SolutionItems) = preProject
+ .github\workflows\stale.yml = .github\workflows\stale.yml
+ .github\workflows\pr.yml = .github\workflows\pr.yml
+ .github\workflows\packages.yml = .github\workflows\packages.yml
+ .github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
+ .github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
+ .github\workflows\docker-ca.yml = .github\workflows\docker-ca.yml
+ .github\workflows\bounty.yml = .github\workflows\bounty.yml
+ .github\workflows\copilot-setup-steps.yml = .github\workflows\copilot-setup-steps.yml
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issue_templates", "issue_templates", "{36556068-64BF-4D4B-827E-EC8CDC23D516}"
+ ProjectSection(SolutionItems) = preProject
+ .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md
+ .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md
+ .github\ISSUE_TEMPLATE\improvement.md = .github\ISSUE_TEMPLATE\improvement.md
+ .github\ISSUE_TEMPLATE\performance.md = .github\ISSUE_TEMPLATE\performance.md
+ .github\ISSUE_TEMPLATE\task.md = .github\ISSUE_TEMPLATE\task.md
+ .github\ISSUE_TEMPLATE\documentation.md = .github\ISSUE_TEMPLATE\documentation.md
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -721,6 +738,8 @@ Global
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B} = {18453B51-25EB-4317-A4B3-B10518252E92}
{9AEE941A-3F23-4E4B-9B59-1A2F57FC762D} = {18453B51-25EB-4317-A4B3-B10518252E92}
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
+ {2158FC82-6253-47F2-9216-B266710D50F1} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
+ {36556068-64BF-4D4B-827E-EC8CDC23D516} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/src/modules/Elsa.Workflows.Core/Activities/For.cs b/src/modules/Elsa.Workflows.Core/Activities/For.cs
index a607c73e6..0dcef9454 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/For.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/For.cs
@@ -26,9 +26,9 @@ public class For : Activity
///
public For(int start, int end, int step, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line)
{
- Start = new Input(start);
- End = new Input(end);
- Step = new Input(step);
+ Start = new(start);
+ End = new(end);
+ Step = new(step);
}
///
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Fork.cs b/src/modules/Elsa.Workflows.Core/Activities/Fork.cs
index 8f8f6e98b..da7fbe8d9 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/Fork.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/Fork.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Runtime.CompilerServices;
+using Elsa.Extensions;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Signals;
using Elsa.Workflows.UIHints;
@@ -38,7 +39,17 @@ public class Fork : Activity
public ICollection Branches { get; set; } = new List();
///
- protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => context.ScheduleActivities(Branches, CompleteChildAsync);
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ // If there are no branches, complete immediately
+ if (Branches.Count == 0)
+ {
+ await context.CompleteActivityAsync();
+ return;
+ }
+
+ await context.ScheduleActivities(Branches, CompleteChildAsync);
+ }
private async ValueTask CompleteChildAsync(ActivityCompletedContext context)
{
@@ -57,7 +68,7 @@ public class Fork : Activity
// Append activity to the set of completed activities.
var completedActivityIds = targetContext.UpdateProperty>("Completed", set =>
{
- set ??= new HashSet();
+ set ??= new();
set.Add(completedChildActivityId);
return set;
});
diff --git a/src/modules/Elsa.Workflows.Core/Activities/While.cs b/src/modules/Elsa.Workflows.Core/Activities/While.cs
index 215160f2a..3dc28bab1 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/While.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/While.cs
@@ -21,7 +21,7 @@ public class While : Activity
///
public static While True(IActivity body) => new(body)
{
- Condition = new Input(true)
+ Condition = new(true)
};
///
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs
index 5233b5e1c..c293c2d96 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs
@@ -1,64 +1,58 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.IntegrationTests.Activities.Workflows;
-using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Activities;
-public class BreakTests
+///
+/// Integration tests for the activity.
+/// Tests Break behavior across different looping constructs (ForEach, For, While, Fork).
+///
+public class BreakTests(ITestOutputHelper testOutputHelper)
{
- private readonly IWorkflowRunner _workflowRunner;
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
- public BreakTests(ITestOutputHelper testOutputHelper)
+ [Fact(DisplayName = "Break exits ForEach loop")]
+ public async Task Break_ExitsForEachLoop()
{
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- _workflowRunner = _services.GetRequiredService();
- }
+ // Act
+ await _fixture.RunWorkflowAsync(new BreakForEachWorkflow());
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
- [Fact(DisplayName = "Break exits out of ForEach")]
- public async Task Test1()
- {
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
+ // Assert
Assert.Equal(new[] { "Start", "C#", "End" }, lines);
}
- [Fact(DisplayName = "Break exits out of immediate ForEach only")]
- public async Task Test2()
+ [Fact(DisplayName = "Break exits only immediate loop in nested ForEach")]
+ public async Task Break_ExitsOnlyImmediateLoopInNestedForEach()
{
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
+ // Act
+ await _fixture.RunWorkflowAsync(new NestedForEachWithBreakWorkflow());
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
+
+ // Assert
Assert.Equal(new[] { "C#", "Classes", "Rust", "Classes", "Go", "Classes" }, lines);
}
- [Fact(DisplayName = "Break exits out of For")]
- public async Task Test3()
+ [Fact(DisplayName = "Break exits For loop")]
+ public async Task Break_ExitsForLoop()
{
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
+ // Act
+ await _fixture.RunWorkflowAsync(new BreakForWorkflow());
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
+
+ // Assert
Assert.Equal(new[] { "Start", "0", "1", "End" }, lines);
}
-
- [Fact(DisplayName = "Break exits out of While")]
- public async Task Test4()
+
+ [Fact(DisplayName = "Break exits While loop")]
+ public async Task Break_ExitsWhileLoop()
{
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
+ // Act
+ await _fixture.RunWorkflowAsync(new BreakWhileWorkflow());
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
+
+ // Assert
Assert.Equal(new[] { "Start", "1", "2", "End" }, lines);
}
-
- [Fact(DisplayName = "Break removes the right bookmarks")]
- public async Task Test5()
- {
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
- Assert.Equal(new[] { "Start", "1", "2", "End" }, lines);
- }
-}
\ No newline at end of file
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs
index 1a00cc3f3..d5b36e141 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs
@@ -3,7 +3,7 @@ using Elsa.Workflows.Memory;
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
-class BreakForEachWorkflow : WorkflowBase
+public class BreakForEachWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs
index 6766297f6..70aa5205b 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs
@@ -3,7 +3,7 @@ using Elsa.Workflows.Memory;
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
-class BreakForWorkflow : WorkflowBase
+public class BreakForWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs
deleted file mode 100644
index ddde01831..000000000
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using Elsa.Workflows.Activities;
-using Elsa.Workflows.Memory;
-using Elsa.Workflows.Runtime.Activities;
-
-namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
-
-public class BreakWhileForkWorkflow : WorkflowBase
-{
- protected override void Build(IWorkflowBuilder workflow)
- {
- var currentValue = new Variable("CurrentValue", 0);
-
- workflow.Root = new Sequence
- {
- Variables = { currentValue },
- Activities =
- {
- While.True(new Fork
- {
- Branches =
- {
- new Sequence
- {
- Activities =
- {
- new SetVariable
- {
- Variable = currentValue,
- Value = new (context => currentValue.Get(context) + 1)
- },
- new If(context => currentValue.Get(context) == 3)
- {
- Then = new Break()
- }
- }
- },
- new Sequence
- {
- Activities =
- {
- new WriteLine("Waiting for event..."),
- new Runtime.Activities.Event("Some event") { Id = "SomeEvent" },
- new WriteLine("Resuming"),
- }
- }
- }
- }),
- }
- };
- }
-}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs
index 38b718be0..701015fe3 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs
@@ -3,7 +3,7 @@ using Elsa.Workflows.Memory;
namespace Elsa.Workflows.IntegrationTests.Activities;
-class NestedForEachWithBreakWorkflow : WorkflowBase
+public class NestedForEachWithBreakWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs
index ffabf4de1..0dbb49064 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs
@@ -1,54 +1,64 @@
using Elsa.Extensions;
using Elsa.Testing.Shared;
+using Elsa.Workflows.IntegrationTests.Activities.Workflows;
using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Activities;
-public class ForkTests
+///
+/// Integration tests for the activity.
+/// Tests Fork behavior with different join modes and branch configurations.
+///
+public class ForkTests(ITestOutputHelper testOutputHelper)
{
- private readonly IWorkflowRunner _workflowRunner;
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
- private readonly IServiceProvider _services;
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
- public ForkTests(ITestOutputHelper testOutputHelper)
+ [Fact(DisplayName = "Fork executes all branches with WaitAll")]
+ public async Task Fork_ExecutesAllBranchesWithWaitAll()
{
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- _workflowBuilderFactory = _services.GetRequiredService();
- _workflowRunner = _services.GetRequiredService();
- }
+ // Act
+ await _fixture.RunWorkflowAsync(new BasicForkWorkflow());
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
- [Fact(DisplayName = "Each branch executes")]
- public async Task Test1()
- {
- await _services.PopulateRegistriesAsync();
- var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync();
- await _workflowRunner.RunAsync(workflow);
- var lines = _capturingTextWriter.Lines.ToList();
+ // Assert
Assert.Equal(new[] { "Branch 1", "Branch 2", "Branch 3" }, lines);
}
- [Fact(DisplayName = "Wait AnyAsync causes workflow to continue")]
- public async Task Test2()
+ [Fact(DisplayName = "Fork with WaitAny continues after first branch completes")]
+ public async Task Fork_WaitAnyContinuesAfterFirstBranch()
{
- await _services.PopulateRegistriesAsync();
- var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync();
+ // Arrange & build services
+ await _fixture.BuildAsync();
+ var workflowBuilderFactory = _fixture.Services.GetRequiredService();
+ var workflow = await workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync();
- // First run.
- var result = await _workflowRunner.RunAsync(workflow);
+ // Act - First run
+ var workflowRunner = _fixture.Services.GetRequiredService();
+ var result = await workflowRunner.RunAsync(workflow);
- // Collect one of the bookmarks to resume the workflow.
+ // Collect one of the bookmarks to resume the workflow
var bookmark = result.WorkflowState.Bookmarks.FirstOrDefault(x => x.ActivityId == "Event2");
Assert.NotNull(bookmark);
- // Resume the workflow.
+ // Resume the workflow
var runOptions = new RunWorkflowOptions { BookmarkId = bookmark.Id };
- await _workflowRunner.RunAsync(workflow, result.WorkflowState, runOptions);
+ await workflowRunner.RunAsync(workflow, result.WorkflowState, runOptions);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
- // Verify output.
- var lines = _capturingTextWriter.Lines.ToList();
+ // Assert
Assert.Equal(new[] { "Start", "Branch 2", "End" }, lines);
}
+
+ [Fact(DisplayName = "Fork with no branches completes successfully")]
+ public async Task Fork_WithNoBranchesCompletesSuccessfully()
+ {
+ // Act
+ await _fixture.RunWorkflowAsync(new EmptyForkWorkflow());
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
+
+ // Assert
+ Assert.Equal(new[] { "Before fork", "After fork" }, lines);
+ }
}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/BasicForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/BasicForkWorkflow.cs
similarity index 85%
rename from test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/BasicForkWorkflow.cs
rename to test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/BasicForkWorkflow.cs
index 2cbb9763a..789aebae7 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/BasicForkWorkflow.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/BasicForkWorkflow.cs
@@ -1,6 +1,6 @@
using Elsa.Workflows.Activities;
-namespace Elsa.Workflows.IntegrationTests.Activities;
+namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
public class BasicForkWorkflow : WorkflowBase
{
@@ -16,4 +16,4 @@ public class BasicForkWorkflow : WorkflowBase
}
};
}
-}
\ No newline at end of file
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs
new file mode 100644
index 000000000..62fb163d1
--- /dev/null
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs
@@ -0,0 +1,19 @@
+using Elsa.Workflows.Activities;
+
+namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
+
+public class EmptyForkWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder workflow)
+ {
+ workflow.Root = new Sequence
+ {
+ Activities =
+ {
+ new WriteLine("Before fork"),
+ new Fork(),
+ new WriteLine("After fork")
+ }
+ };
+ }
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/JoinAnyForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/JoinAnyForkWorkflow.cs
similarity index 96%
rename from test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/JoinAnyForkWorkflow.cs
rename to test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/JoinAnyForkWorkflow.cs
index 72c7706be..e37e90262 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/JoinAnyForkWorkflow.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/JoinAnyForkWorkflow.cs
@@ -1,7 +1,7 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;
-namespace Elsa.Workflows.IntegrationTests.Activities;
+namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
public class JoinAnyForkWorkflow : WorkflowBase
{
@@ -56,4 +56,4 @@ public class JoinAnyForkWorkflow : WorkflowBase
}
};
}
-}
\ No newline at end of file
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs
index 80c77f707..8fb0457b1 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs
@@ -36,7 +36,7 @@ public class BreakWhileFromForkWorkflow : WorkflowBase
new Event("Branch 1") { Id = "Branch 1" },
new WriteLine("Branch 1 - Resumed"),
- // This should break the while loop, not matter how high up in the tree it is.
+ // This should break the while loop, no matter how high up in the tree it is.
new Break()
}
},
diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs b/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs
index 21778bf96..1b9c53d77 100644
--- a/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs
+++ b/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs
@@ -1,20 +1,18 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
-using Elsa.Workflows.Signals;
namespace Elsa.Activities.UnitTests.Flow;
///
-/// Unit tests for the Fork activity covering various join modes, branching scenarios, and signal handling.
+/// Unit tests for the activity.
///
public class ForkTests
{
- [Fact]
- public async Task SchedulesAllBranches()
+ [Fact(DisplayName = "Fork schedules all branches")]
+ public async Task Fork_SchedulesAllBranches()
{
// Arrange
var branches = CreateBranches(3);
-
var fork = new Fork
{
JoinMode = ForkJoinMode.WaitAll,
@@ -31,36 +29,30 @@ public class ForkTests
}
}
- [Fact]
- public async Task ExecutesWithNoBranches()
+ [Fact(DisplayName = "Fork with no branches completes immediately")]
+ public async Task Fork_WithNoBranchesCompletesImmediately()
{
// Arrange
- var fork = new Fork
- {
- JoinMode = ForkJoinMode.WaitAll
- };
+ var fork = new Fork { JoinMode = ForkJoinMode.WaitAll };
// Act
var context = await ExecuteForkAsync(fork);
// Assert
- Assert.NotNull(context);
+ Assert.Equal(ActivityStatus.Completed, context.Status);
}
- [Theory]
+ [Theory(DisplayName = "Fork respects join mode")]
[InlineData(ForkJoinMode.WaitAll)]
[InlineData(ForkJoinMode.WaitAny)]
- public async Task ExecutesWithSpecifiedJoinMode(ForkJoinMode joinMode)
+ public async Task Fork_RespectsJoinMode(ForkJoinMode joinMode)
{
// Arrange
var branch = new WriteLine("Test Branch");
var fork = new Fork
{
JoinMode = joinMode,
- Branches =
- {
- branch
- }
+ Branches = { branch }
};
// Act
@@ -71,305 +63,11 @@ public class ForkTests
Assert.True(context.HasScheduledActivity(branch));
}
- [Theory]
- [InlineData(ForkJoinMode.WaitAll, 1, 1)] // WaitAll: complete 1 of 2, should track 1
- [InlineData(ForkJoinMode.WaitAll, 2, 2)] // WaitAll: complete 2 of 2, should track 2
- [InlineData(ForkJoinMode.WaitAny, 1, 1)] // WaitAny: complete 1 of 2, should track 1
- public async Task CompletionTrackingWithJoinModes(ForkJoinMode joinMode, int branchesToComplete, int expectedCompleted)
- {
- // Arrange
- var branches = CreateBranches(2);
- var fork = new Fork
- {
- JoinMode = joinMode,
- Branches = branches.Cast().ToList()
- };
- var context = await ExecuteForkAsync(fork);
+ private static Task ExecuteForkAsync(Fork fork) =>
+ new ActivityTestFixture(fork).ExecuteAsync();
- // Act
- HashSet completedSet = null!;
- for (var i = 0; i < branchesToComplete; i++)
- {
- completedSet = await CompleteBranchAsync(fork, context, branches[i]);
- }
-
- // Assert
- Assert.NotNull(completedSet);
- Assert.Equal(expectedCompleted, completedSet.Count);
- for (var i = 0; i < branchesToComplete; i++)
- {
- Assert.Contains(branches[i].Id, completedSet);
- }
-
- for (var i = branchesToComplete; i < branches.Length; i++)
- {
- Assert.DoesNotContain(branches[i].Id, completedSet);
- }
- }
-
- [Fact]
- public async Task HandlesBreakSignal()
- {
- // Arrange
- var fork = new Fork
- {
- Branches =
- {
- new WriteLine("Branch")
- }
- };
- var context = await ExecuteForkAsync(fork);
-
- // Act
- var breakSignal = new BreakSignal();
- var signalContext = new SignalContext(context, context, CancellationToken.None);
- await InvokePrivateMethodAsync(fork, "OnBreakSignalReceived", breakSignal, signalContext);
-
- // Assert
- Assert.True(context.GetIsBreaking());
- }
-
- [Fact]
- public async Task CompletesImmediatelyWhenBreaking()
- {
- // Arrange
- var branch = new WriteLine("Branch");
- var fork = new Fork
- {
- Branches =
- {
- branch
- }
- };
- var context = await ExecuteForkAsync(fork);
- context.SetIsBreaking();
-
- // Act
- await CompleteBranchAsync(fork, context, branch);
-
- // Assert
- Assert.True(context.GetIsBreaking());
- }
-
- [Fact]
- public async Task SingleBranchCompletesCorrectly()
- {
- // Arrange
- var branch = new WriteLine("Single Branch");
- var fork = new Fork
- {
- JoinMode = ForkJoinMode.WaitAll,
- Branches =
- {
- branch
- }
- };
- var context = await ExecuteForkAsync(fork);
-
- // Act
- var completedSet = await CompleteBranchAsync(fork, context, branch);
-
- // Assert
- Assert.NotNull(completedSet);
- Assert.Single(completedSet);
- Assert.Contains(branch.Id, completedSet);
- }
-
- [Theory]
- [InlineData(ForkJoinMode.WaitAll, 3, 3)] // WaitAll requires all 3 branches to complete
- [InlineData(ForkJoinMode.WaitAny, 3, 1)] // WaitAny only needs 1 branch to complete
- public async Task HandlesMultipleBranchesWithJoinModes(ForkJoinMode joinMode, int totalBranches, int branchesToComplete)
- {
- // Arrange
- var branches = CreateBranches(totalBranches);
- var fork = new Fork
- {
- JoinMode = joinMode,
- Branches = branches.Cast().ToList()
- };
- var context = await ExecuteForkAsync(fork);
-
- // Act
- HashSet completedSet = null!;
- for (var i = 0; i < branchesToComplete; i++)
- {
- completedSet = await CompleteBranchAsync(fork, context, branches[i]);
- }
-
- // Assert
- Assert.NotNull(completedSet);
- Assert.Equal(branchesToComplete, completedSet.Count);
- for (var i = 0; i < branchesToComplete; i++)
- {
- Assert.Contains(branches[i].Id, completedSet);
- }
- }
-
- [Fact]
- public async Task TracksAllActivityCompletions() // Including non-branch activities
- {
- // Arrange
- var branch = new WriteLine("Valid Branch")
- {
- Id = "branch-activity"
- };
- var nonBranch = new WriteLine("Non-Branch Activity")
- {
- Id = "non-branch-activity"
- };
- var fork = new Fork
- {
- JoinMode = ForkJoinMode.WaitAll,
- Branches =
- {
- branch
- }
- };
- var context = await ExecuteForkAsync(fork);
-
- // Act - complete a non-branch activity
- var completedSet = await CompleteBranchAsync(fork, context, nonBranch);
-
- // Assert - Fork tracks ALL completions, even non-branch activities
- Assert.NotNull(completedSet);
- Assert.Contains(nonBranch.Id, completedSet);
- Assert.Single(completedSet);
- }
-
- [Fact]
- public async Task JoinLogicOnlyConsidersBranchActivities()
- {
- // Arrange
- var branch = new WriteLine("Valid Branch")
- {
- Id = "branch-activity"
- };
- var nonBranch = new WriteLine("Non-Branch Activity")
- {
- Id = "non-branch-activity"
- };
- var fork = new Fork
- {
- JoinMode = ForkJoinMode.WaitAll,
- Branches =
- {
- branch
- }
- };
- var context = await ExecuteForkAsync(fork);
-
- // Act - complete non-branch activity first, then branch activity
- await CompleteBranchAsync(fork, context, nonBranch);
- var completedSet = await CompleteBranchAsync(fork, context, branch);
-
- // Assert - both activities should be tracked
- Assert.NotNull(completedSet);
- Assert.Contains(branch.Id, completedSet);
- Assert.Contains(nonBranch.Id, completedSet);
- Assert.Equal(2, completedSet.Count);
- }
-
- [Fact]
- public async Task MaintainsSeparateCompletionTracking()
- {
- // Arrange
- var branch1 = new WriteLine("Branch 1")
- {
- Id = "branch1-unique"
- };
- var branch2 = new WriteLine("Branch 2")
- {
- Id = "branch2-unique"
- };
- var fork1 = new Fork
- {
- Branches =
- {
- branch1
- }
- };
- var fork2 = new Fork
- {
- Branches =
- {
- branch2
- }
- };
- var context1 = await ExecuteForkAsync(fork1);
- var context2 = await ExecuteForkAsync(fork2);
-
- // Act
- var completed1 = await CompleteBranchAsync(fork1, context1, branch1);
- var completed2 = await CompleteBranchAsync(fork2, context2, branch2);
-
- // Assert - each fork should track its own completions
- Assert.NotNull(completed1);
- Assert.NotNull(completed2);
- Assert.Contains(branch1.Id, completed1);
- Assert.DoesNotContain(branch2.Id, completed1);
- Assert.Contains(branch2.Id, completed2);
- Assert.DoesNotContain(branch1.Id, completed2);
- }
-
- private static async Task ExecuteForkAsync(Fork fork)
- {
- var fixture = new ActivityTestFixture(fork);
- return await fixture.ExecuteAsync();
- }
-
- private static WriteLine[] CreateBranches(int count, string namePrefix = "Branch")
- {
- return Enumerable
- .Range(1, count)
- .Select(i => new WriteLine($"{namePrefix} {i}")
- {
- Id = $"{namePrefix.ToLower()}-{i}"
- })
+ private static WriteLine[] CreateBranches(int count) =>
+ Enumerable.Range(1, count)
+ .Select(i => new WriteLine($"Branch {i}") { Id = $"branch-{i}" })
.ToArray();
- }
-
- private static async Task CreateChildContextAsync(IActivity childActivity)
- {
- var childFixture = new ActivityTestFixture(childActivity);
- var childContext = await childFixture.ExecuteAsync();
- return childContext;
- }
-
- private static async Task> CompleteBranchAsync(Fork fork, ActivityExecutionContext parentContext, IActivity branch)
- {
- var childContext = await CreateChildContextAsync(branch);
- var completedContext = new ActivityCompletedContext(parentContext, childContext);
- await InvokeCompleteChildAsync(fork, completedContext);
- return parentContext.GetProperty>("Completed") ?? new HashSet();
- }
-
- private static async Task InvokePrivateMethodAsync(object instance, string methodName, params object[] parameters)
- {
- var method = instance.GetType().GetMethod(methodName,
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
- ?? throw new InvalidOperationException($"{methodName} method not found on {instance.GetType().Name} class. This may indicate a breaking change in the implementation.");
-
- try
- {
- var result = method.Invoke(instance, parameters);
- switch (result)
- {
- case ValueTask valueTask:
- await valueTask;
- break;
- case Task task:
- await task;
- break;
- }
- }
- catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException != null)
- {
- throw ex.InnerException;
- }
- }
-
- private static Task InvokeCompleteChildAsync(Fork fork, ActivityCompletedContext completedContext)
- {
- return InvokePrivateMethodAsync(fork, "CompleteChildAsync", completedContext);
- }
-}
\ No newline at end of file
+}
diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs
new file mode 100644
index 000000000..5730e1b3b
--- /dev/null
+++ b/test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs
@@ -0,0 +1,35 @@
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+
+namespace Elsa.Activities.UnitTests.Looping;
+
+///
+/// Unit tests for the activity.
+///
+public class BreakTests
+{
+ [Fact(DisplayName = "Break implements ITerminalNode interface")]
+ public void Break_ImplementsITerminalNode()
+ {
+ // Arrange
+ var breakActivity = new Break();
+
+ // Assert
+ Assert.IsAssignableFrom(breakActivity);
+ }
+
+ [Fact(DisplayName = "Break completes execution")]
+ public async Task Break_CompletesExecution()
+ {
+ // Arrange
+ var breakActivity = new Break();
+ var fixture = new ActivityTestFixture(breakActivity);
+
+ // Act
+ var context = await fixture.ExecuteAsync();
+
+ // Assert
+ Assert.Equal(ActivityStatus.Completed, context.Status);
+ }
+}