diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0887bd..7842ac2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## 3.2.1 - 2025.04.24
+
+### Modified
+
+- Code readability improvements in `WorkflowBuilder`
+
## 3.2.0 - 2025-04-24
### Added
diff --git a/Directory.Build.props b/Directory.Build.props
index 0445d1e..ead2781 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -13,7 +13,7 @@
A .NET library for building robust, functional workflows and processing pipelines.
- 3.2.0
+ 3.2.1
true
diff --git a/Zooper.Bee.Example/ParameterlessWorkflowExample.cs b/Zooper.Bee.Example/ParameterlessWorkflowExample.cs
index d28e749..5dc748c 100644
--- a/Zooper.Bee.Example/ParameterlessWorkflowExample.cs
+++ b/Zooper.Bee.Example/ParameterlessWorkflowExample.cs
@@ -1,8 +1,11 @@
using Zooper.Fox;
+using Zooper.Bee.Extensions;
+
+// ReSharper disable ClassNeverInstantiated.Global
namespace Zooper.Bee.Example;
-public class ParameterlessWorkflowExample
+public sealed class ParameterlessWorkflowExample
{
// Success model
public record ProcessingResult(DateTime ProcessedAt, string Status);
@@ -43,17 +46,28 @@ private static async Task RunExampleWithFactory()
// Configure the workflow
builder => builder
.Do(payload =>
- {
- Console.WriteLine("Processing step 1...");
- return Either.FromRight(
- payload with { Status = "Step 1 completed" });
- })
+ {
+ Console.WriteLine("Processing step 1...");
+ return Either.FromRight(
+ payload with
+ {
+ Status = "Step 1 completed"
+ }
+ );
+ }
+ )
.Do(payload =>
- {
- Console.WriteLine("Processing step 2...");
- return Either.FromRight(
- payload with { Status = "Step 2 completed", IsCompleted = true });
- })
+ {
+ Console.WriteLine("Processing step 2...");
+ return Either.FromRight(
+ payload with
+ {
+ Status = "Step 2 completed",
+ IsCompleted = true
+ }
+ );
+ }
+ )
);
// Execute without parameters
@@ -74,25 +88,36 @@ private static async Task RunExampleWithUnit()
{
// Create a workflow with Unit type as request
var workflow = new WorkflowBuilder(
- // Use Unit parameter (ignored)
- _ => new ProcessingPayload(StartedAt: DateTime.UtcNow),
+ // Use Unit parameter (ignored)
+ _ => new ProcessingPayload(StartedAt: DateTime.UtcNow),
- // Result selector
- payload => new ProcessingResult(DateTime.UtcNow, payload.Status)
- )
- .Do(payload =>
- {
- Console.WriteLine("Executing task A...");
- return Either.FromRight(
- payload with { Status = "Task A completed" });
- })
- .Do(payload =>
- {
- Console.WriteLine("Executing task B...");
- return Either.FromRight(
- payload with { Status = "Task B completed", IsCompleted = true });
- })
- .Build();
+ // Result selector
+ payload => new ProcessingResult(DateTime.UtcNow, payload.Status)
+ )
+ .Do(payload =>
+ {
+ Console.WriteLine("Executing task A...");
+ return Either.FromRight(
+ payload with
+ {
+ Status = "Task A completed"
+ }
+ );
+ }
+ )
+ .Do(payload =>
+ {
+ Console.WriteLine("Executing task B...");
+ return Either.FromRight(
+ payload with
+ {
+ Status = "Task B completed",
+ IsCompleted = true
+ }
+ );
+ }
+ )
+ .Build();
// Execute with Unit.Value
var result = await workflow.Execute(Unit.Value);
@@ -112,25 +137,36 @@ private static async Task RunExampleWithExtension()
{
// Create a workflow with Unit type as request
var workflow = new WorkflowBuilder(
- // Use Unit parameter (ignored)
- _ => new ProcessingPayload(StartedAt: DateTime.UtcNow),
+ // Use Unit parameter (ignored)
+ _ => new ProcessingPayload(StartedAt: DateTime.UtcNow),
- // Result selector
- payload => new ProcessingResult(DateTime.UtcNow, payload.Status)
- )
- .Do(payload =>
- {
- Console.WriteLine("Running process X...");
- return Either.FromRight(
- payload with { Status = "Process X completed" });
- })
- .Do(payload =>
- {
- Console.WriteLine("Running process Y...");
- return Either.FromRight(
- payload with { Status = "Process Y completed", IsCompleted = true });
- })
- .Build();
+ // Result selector
+ payload => new ProcessingResult(DateTime.UtcNow, payload.Status)
+ )
+ .Do(payload =>
+ {
+ Console.WriteLine("Running process X...");
+ return Either.FromRight(
+ payload with
+ {
+ Status = "Process X completed"
+ }
+ );
+ }
+ )
+ .Do(payload =>
+ {
+ Console.WriteLine("Running process Y...");
+ return Either.FromRight(
+ payload with
+ {
+ Status = "Process Y completed",
+ IsCompleted = true
+ }
+ );
+ }
+ )
+ .Build();
// Execute using the extension method (no parameters)
var result = await workflow.Execute();
diff --git a/Zooper.Bee.Tests/ParameterlessWorkflowTests.cs b/Zooper.Bee.Tests/ParameterlessWorkflowTests.cs
index 0c92d92..e8f6a1d 100644
--- a/Zooper.Bee.Tests/ParameterlessWorkflowTests.cs
+++ b/Zooper.Bee.Tests/ParameterlessWorkflowTests.cs
@@ -3,12 +3,14 @@
using FluentAssertions;
using Xunit;
using Zooper.Fox;
+using Zooper.Bee.Extensions;
namespace Zooper.Bee.Tests;
public class ParameterlessWorkflowTests
{
#region Test Models
+
// Payload model for tests
private record TestPayload(DateTime StartTime, string Status = "Waiting");
@@ -17,6 +19,7 @@ private record TestSuccess(string Status, bool IsComplete);
// Error model
private record TestError(string Code, string Message);
+
#endregion
[Fact]
@@ -24,17 +27,27 @@ public async Task ParameterlessWorkflow_UsingUnitType_CanBeExecuted()
{
// Arrange
var workflow = new WorkflowBuilder(
- // Convert Unit to initial payload
- _ => new TestPayload(DateTime.UtcNow),
-
- // Convert final payload to success result
- payload => new TestSuccess(payload.Status, true)
- )
- .Do(payload => Either.FromRight(
- payload with { Status = "Processing" }))
- .Do(payload => Either.FromRight(
- payload with { Status = "Completed" }))
- .Build();
+ // Convert Unit to initial payload
+ _ => new TestPayload(DateTime.UtcNow),
+
+ // Convert final payload to success result
+ payload => new TestSuccess(payload.Status, true)
+ )
+ .Do(payload => Either.FromRight(
+ payload with
+ {
+ Status = "Processing"
+ }
+ )
+ )
+ .Do(payload => Either.FromRight(
+ payload with
+ {
+ Status = "Completed"
+ }
+ )
+ )
+ .Build();
// Act
var result = await workflow.Execute(Unit.Value);
@@ -59,9 +72,19 @@ public async Task ParameterlessWorkflow_UsingFactory_CanBeExecuted()
// Configure the workflow
builder => builder
.Do(payload => Either.FromRight(
- payload with { Status = "Processing" }))
+ payload with
+ {
+ Status = "Processing"
+ }
+ )
+ )
.Do(payload => Either.FromRight(
- payload with { Status = "Completed" }))
+ payload with
+ {
+ Status = "Completed"
+ }
+ )
+ )
);
// Act
@@ -78,14 +101,24 @@ public async Task ParameterlessWorkflow_UsingExtensionMethod_CanBeExecuted()
{
// Arrange
var workflow = new WorkflowBuilder(
- _ => new TestPayload(DateTime.UtcNow),
- payload => new TestSuccess(payload.Status, true)
- )
- .Do(payload => Either.FromRight(
- payload with { Status = "Processing" }))
- .Do(payload => Either.FromRight(
- payload with { Status = "Completed" }))
- .Build();
+ _ => new TestPayload(DateTime.UtcNow),
+ payload => new TestSuccess(payload.Status, true)
+ )
+ .Do(payload => Either.FromRight(
+ payload with
+ {
+ Status = "Processing"
+ }
+ )
+ )
+ .Do(payload => Either.FromRight(
+ payload with
+ {
+ Status = "Completed"
+ }
+ )
+ )
+ .Build();
// Act - using extension method (no parameters)
var result = await workflow.Execute();
@@ -101,18 +134,25 @@ public async Task ParameterlessWorkflow_WithError_ReturnsError()
{
// Arrange
var workflow = WorkflowBuilderFactory.Create(
- () => new TestPayload(DateTime.UtcNow),
- payload => new TestSuccess(payload.Status, true)
- )
- .Do(payload => Either.FromRight(
- payload with { Status = "Processing" }))
- .Do(payload =>
- {
- // Simulate an error in the workflow
- return Either.FromLeft(
- new TestError("PROCESSING_FAILED", "Failed to complete processing"));
- })
- .Build();
+ () => new TestPayload(DateTime.UtcNow),
+ payload => new TestSuccess(payload.Status, true)
+ )
+ .Do(payload => Either.FromRight(
+ payload with
+ {
+ Status = "Processing"
+ }
+ )
+ )
+ .Do(payload =>
+ {
+ // Simulate an error in the workflow
+ return Either.FromLeft(
+ new TestError("PROCESSING_FAILED", "Failed to complete processing")
+ );
+ }
+ )
+ .Build();
// Act
var result = await workflow.Execute();
diff --git a/Zooper.Bee/BranchWithLocalPayloadBuilder.cs b/Zooper.Bee/BranchWithLocalPayloadBuilder.cs
index cb5559a..f6f7f71 100644
--- a/Zooper.Bee/BranchWithLocalPayloadBuilder.cs
+++ b/Zooper.Bee/BranchWithLocalPayloadBuilder.cs
@@ -16,14 +16,10 @@ namespace Zooper.Bee;
/// The type of the error result
public sealed class BranchWithLocalPayloadBuilder
{
- private readonly WorkflowBuilder _workflow;
private readonly BranchWithLocalPayload _branch;
- internal BranchWithLocalPayloadBuilder(
- WorkflowBuilder workflow,
- BranchWithLocalPayload branch)
+ internal BranchWithLocalPayloadBuilder(BranchWithLocalPayload branch)
{
- _workflow = workflow;
_branch = branch;
}
@@ -35,7 +31,7 @@ internal BranchWithLocalPayloadBuilder(
public BranchWithLocalPayloadBuilder Do(
Func>> activity)
{
- _branch.Activities.Add(new BranchActivity(activity));
+ _branch.Activities.Add(new(activity));
return this;
}
@@ -47,9 +43,13 @@ public BranchWithLocalPayloadBuilder Do(
Func> activity)
{
- _branch.Activities.Add(new BranchActivity(
- (mainPayload, localPayload, _) => Task.FromResult(activity(mainPayload, localPayload))
- ));
+ _branch.Activities.Add(
+ new((
+ mainPayload,
+ localPayload,
+ _) => Task.FromResult(activity(mainPayload, localPayload))
+ )
+ );
return this;
}
@@ -59,12 +59,14 @@ public BranchWithLocalPayloadBuilderThe activities to add
/// The branch builder for fluent chaining
public BranchWithLocalPayloadBuilder DoAll(
- params Func>>[] activities)
+ params Func>>[]
+ activities)
{
foreach (var activity in activities)
{
- _branch.Activities.Add(new BranchActivity(activity));
+ _branch.Activities.Add(new(activity));
}
+
return this;
}
@@ -78,10 +80,15 @@ public BranchWithLocalPayloadBuilder(
- (mainPayload, localPayload, _) => Task.FromResult(activity(mainPayload, localPayload))
- ));
+ _branch.Activities.Add(
+ new((
+ mainPayload,
+ localPayload,
+ _) => Task.FromResult(activity(mainPayload, localPayload))
+ )
+ );
}
+
return this;
}
}
\ No newline at end of file
diff --git a/Zooper.Bee/Extensions/WorkflowExtensions.cs b/Zooper.Bee/Extensions/WorkflowExtensions.cs
index 7665d9d..706e9da 100644
--- a/Zooper.Bee/Extensions/WorkflowExtensions.cs
+++ b/Zooper.Bee/Extensions/WorkflowExtensions.cs
@@ -2,7 +2,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
+using Zooper.Fox;
namespace Zooper.Bee.Extensions;
@@ -13,6 +16,33 @@ namespace Zooper.Bee.Extensions;
///
public static class WorkflowExtensions
{
+ ///
+ /// Executes a workflow that doesn't require a request parameter.
+ ///
+ /// The type of the success result
+ /// The type of the error result
+ /// The workflow to execute
+ /// The result of the workflow execution
+ public static Task> Execute(this Workflow workflow)
+ {
+ return workflow.Execute(Unit.Value);
+ }
+
+ ///
+ /// Executes a workflow that doesn't require a request parameter.
+ ///
+ /// The type of the success result
+ /// The type of the error result
+ /// The workflow to execute
+ /// A cancellation token to observe while waiting for the task to complete
+ /// The result of the workflow execution
+ public static Task> Execute(
+ this Workflow workflow,
+ CancellationToken cancellationToken)
+ {
+ return workflow.Execute(Unit.Value, cancellationToken);
+ }
+
///
/// Registers all workflow components from the specified assemblies into the service collection.
/// This includes workflow validations, workflow activities, and concrete workflow classes.
diff --git a/Zooper.Bee/WorkflowBuilder.cs b/Zooper.Bee/WorkflowBuilder.cs
index c6835f6..04fc8b6 100644
--- a/Zooper.Bee/WorkflowBuilder.cs
+++ b/Zooper.Bee/WorkflowBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Zooper.Bee.Features;
@@ -7,6 +8,8 @@
using Zooper.Bee.Internal.Executors;
using Zooper.Fox;
+// ReSharper disable MemberCanBePrivate.Global
+
namespace Zooper.Bee;
///
@@ -63,7 +66,7 @@ public WorkflowBuilder(
public WorkflowBuilder Validate(
Func>> validation)
{
- _validations.Add(new WorkflowValidation(validation));
+ _validations.Add(new(validation));
return this;
}
@@ -72,12 +75,14 @@ public WorkflowBuilder Validate(
///
/// The validation function
/// The builder instance for method chaining
- public WorkflowBuilder Validate(
- Func> validation)
+ public WorkflowBuilder Validate(Func> validation)
{
- _validations.Add(new WorkflowValidation(
- (request, _) => Task.FromResult(validation(request))
- ));
+ _validations.Add(
+ new((
+ request,
+ _) => Task.FromResult(validation(request))
+ )
+ );
return this;
}
@@ -89,7 +94,7 @@ public WorkflowBuilder Validate(
public WorkflowBuilder Do(
Func>> activity)
{
- _activities.Add(new WorkflowActivity(activity));
+ _activities.Add(new(activity));
return this;
}
@@ -98,12 +103,14 @@ public WorkflowBuilder Do(
///
/// The activity function
/// The builder instance for method chaining
- public WorkflowBuilder Do(
- Func> activity)
+ public WorkflowBuilder Do(Func> activity)
{
- _activities.Add(new WorkflowActivity(
- (payload, _) => Task.FromResult(activity(payload))
- ));
+ _activities.Add(
+ new((
+ payload,
+ _) => Task.FromResult(activity(payload))
+ )
+ );
return this;
}
@@ -117,8 +124,9 @@ public WorkflowBuilder DoAll(
{
foreach (var activity in activities)
{
- _activities.Add(new WorkflowActivity(activity));
+ _activities.Add(new(activity));
}
+
return this;
}
@@ -127,15 +135,18 @@ public WorkflowBuilder DoAll(
///
/// The activity functions
/// The builder instance for method chaining
- public WorkflowBuilder DoAll(
- params Func>[] activities)
+ public WorkflowBuilder DoAll(params Func>[] activities)
{
foreach (var activity in activities)
{
- _activities.Add(new WorkflowActivity(
- (payload, _) => Task.FromResult(activity(payload))
- ));
+ _activities.Add(
+ new((
+ payload,
+ _) => Task.FromResult(activity(payload))
+ )
+ );
}
+
return this;
}
@@ -150,9 +161,9 @@ public WorkflowBuilder DoIf(
Func>> activity)
{
_conditionalActivities.Add(
- new ConditionalWorkflowActivity(
+ new(
condition,
- new WorkflowActivity(activity)
+ new(activity)
)
);
return this;
@@ -169,10 +180,11 @@ public WorkflowBuilder DoIf(
Func> activity)
{
_conditionalActivities.Add(
- new ConditionalWorkflowActivity(
+ new(
condition,
- new WorkflowActivity(
- (payload, _) => Task.FromResult(activity(payload))
+ new((
+ payload,
+ _) => Task.FromResult(activity(payload))
)
)
);
@@ -189,7 +201,7 @@ public BranchBuilder Branch(Func(condition);
_branches.Add(branch);
- return new BranchBuilder(this, branch);
+ return new(this, branch);
}
///
@@ -264,7 +276,7 @@ public BranchWithLocalPayloadBuilder(condition, localPayloadFactory);
_branchesWithLocalPayload.Add(branch);
- return new BranchWithLocalPayloadBuilder(this, branch);
+ return new(branch);
}
///
@@ -283,7 +295,7 @@ public WorkflowBuilder BranchWithLocalPayl
{
var branch = new BranchWithLocalPayload(condition, localPayloadFactory);
_branchesWithLocalPayload.Add(branch);
- var branchBuilder = new BranchWithLocalPayloadBuilder(this, branch);
+ var branchBuilder = new BranchWithLocalPayloadBuilder(branch);
branchConfiguration(branchBuilder);
return this;
}
@@ -301,7 +313,7 @@ public BranchWithLocalPayloadBuilder(_ => true, localPayloadFactory);
_branchesWithLocalPayload.Add(branch);
- return new BranchWithLocalPayloadBuilder(this, branch);
+ return new(branch);
}
///
@@ -386,7 +398,7 @@ public WorkflowBuilder Detach(
///
/// Creates a parallel execution of multiple groups with an optional condition.
- /// All groups execute in parallel and their results are merged back into the main workflow.
+ /// All groups execute in parallel, and their results are merged back into the main workflow.
///
/// The condition to evaluate. If null, the parallel execution always occurs.
/// An action that configures the parallel execution
@@ -404,7 +416,7 @@ public WorkflowBuilder Parallel(
///
/// Creates a parallel execution of multiple groups that always executes.
- /// All groups execute in parallel and their results are merged back into the main workflow.
+ /// All groups execute in parallel, and their results are merged back into the main workflow.
///
/// An action that configures the parallel execution
/// The workflow builder to continue the workflow definition
@@ -416,7 +428,7 @@ public WorkflowBuilder Parallel(
///
/// Creates a parallel execution of multiple detached groups with an optional condition.
- /// All detached groups execute in parallel and their results are NOT merged back.
+ /// All detached groups execute in parallel, and their results are NOT merged back.
///
/// The condition to evaluate. If null, the parallel detached execution always occurs.
/// An action that configures the parallel detached execution
@@ -427,14 +439,15 @@ public WorkflowBuilder ParallelDetached(
{
var parallelDetached = new Features.Parallel.ParallelDetached(condition);
_features.Add(parallelDetached);
- var parallelDetachedBuilder = new Features.Parallel.ParallelDetachedBuilder(this, parallelDetached);
+ var parallelDetachedBuilder =
+ new Features.Parallel.ParallelDetachedBuilder(this, parallelDetached);
parallelDetachedConfiguration(parallelDetachedBuilder);
return this;
}
///
/// Creates a parallel execution of multiple detached groups that always executes.
- /// All detached groups execute in parallel and their results are NOT merged back.
+ /// All detached groups execute in parallel, and their results are NOT merged back.
///
/// An action that configures the parallel detached execution
/// The workflow builder to continue the workflow definition
@@ -445,475 +458,343 @@ public WorkflowBuilder ParallelDetached(
}
///
- /// Adds an activity to the finally block that will always execute, even if the workflow fails.
+ /// Adds an activity to the "finally" block that will always execute, even if the workflow fails.
///
/// The activity to execute
/// The builder instance for method chaining
public WorkflowBuilder Finally(
Func>> activity)
{
- _finallyActivities.Add(new WorkflowActivity(activity));
+ _finallyActivities.Add(new(activity));
return this;
}
///
- /// Adds a synchronous activity to the finally block that will always execute, even if the workflow fails.
+ /// Adds a synchronous activity to the "finally" block that will always execute, even if the workflow fails.
///
/// The activity to execute
/// The builder instance for method chaining
- public WorkflowBuilder Finally(
- Func> activity)
+ public WorkflowBuilder Finally(Func> activity)
{
- _finallyActivities.Add(new WorkflowActivity(
- (payload, _) => Task.FromResult(activity(payload))
- ));
+ _finallyActivities.Add(
+ new((
+ payload,
+ _) => Task.FromResult(activity(payload))
+ )
+ );
return this;
}
///
- /// Builds a workflow that can be executed with a request of type .
+ /// Builds a workflow that processes a request and returns either a success or an error.
///
- /// A workflow that can be executed with a request of type .
public Workflow Build()
{
- return new Workflow(
- async (request, cancellationToken) =>
- {
- // Run validations
- foreach (var validation in _validations)
- {
- // Skip null validations
- if (validation == null)
- {
- continue;
- }
-
- var validationResult = await validation.Validate(request, cancellationToken);
- if (validationResult.IsSome)
- {
- var errorValue = validationResult.Value;
- // Skip if error value is null
- if (errorValue == null)
- {
- continue;
- }
- return Either.FromLeft(errorValue);
- }
- }
-
- // Create initial payload
- var payload = _contextFactory(request);
-
- // Skip if payload is null
- if (payload == null)
- {
- // Return a default success with default payload
- return Either.FromRight(_resultSelector(default!));
- }
-
- // Execute main activities
- try
- {
- foreach (var activity in _activities)
- {
- // Skip null activities
- if (activity == null)
- {
- continue;
- }
-
- var activityResult = await activity.Execute(payload, cancellationToken);
- if (activityResult == null)
- {
- continue;
- }
-
- if (activityResult.IsLeft)
- {
- var errorValue = activityResult.Left;
- // Skip if error value is null
- if (errorValue == null)
- {
- continue;
- }
- return Either.FromLeft(errorValue);
- }
-
- // Skip if result is null
- if (activityResult.Right == null)
- {
- continue;
- }
- payload = activityResult.Right;
- }
-
- // Execute conditional activities
- foreach (var conditionalActivity in _conditionalActivities)
- {
- // Skip null conditional activities
- if (conditionalActivity == null)
- {
- continue;
- }
-
- if (conditionalActivity.ShouldExecute(payload))
- {
- // Skip if activity is null
- if (conditionalActivity.Activity == null)
- {
- continue;
- }
-
- var activityResult = await conditionalActivity.Activity.Execute(payload, cancellationToken);
- if (activityResult == null)
- {
- continue;
- }
-
- if (activityResult.IsLeft)
- {
- var errorValue = activityResult.Left;
- // Skip if error value is null
- if (errorValue == null)
- {
- continue;
- }
- return Either.FromLeft(errorValue);
- }
-
- // Skip if result is null
- if (activityResult.Right == null)
- {
- continue;
- }
- payload = activityResult.Right;
- }
- }
-
- // Execute branches
- foreach (var branch in _branches)
- {
- // Skip null branches
- if (branch == null)
- {
- continue;
- }
-
- // Skip if condition is null
- if (branch.Condition == null)
- {
- continue;
- }
-
- if (branch.Condition(payload))
- {
- // Skip if activities collection is null
- if (branch.Activities == null)
- {
- continue;
- }
-
- foreach (var activity in branch.Activities)
- {
- // Skip null activities
- if (activity == null)
- {
- continue;
- }
-
- var activityResult = await activity.Execute(payload, cancellationToken);
- if (activityResult == null)
- {
- continue;
- }
-
- if (activityResult.IsLeft)
- {
- var errorValue = activityResult.Left;
- // Skip if error value is null
- if (errorValue == null)
- {
- continue;
- }
- return Either.FromLeft(errorValue);
- }
-
- // Skip if result is null
- if (activityResult.Right == null)
- {
- continue;
- }
- payload = activityResult.Right;
- }
- }
- }
-
- // Execute branches with local payload
- foreach (var branchObj in _branchesWithLocalPayload)
- {
- // Skip null branch objects
- if (branchObj == null)
- {
- continue;
- }
-
- var branchResult = await ExecuteBranchWithLocalPayloadDynamic(branchObj, payload, cancellationToken);
- if (branchResult == null)
- {
- continue;
- }
-
- if (branchResult.IsLeft)
- {
- var errorValue = branchResult.Left;
- // Skip if error value is null
- if (errorValue == null)
- {
- continue;
- }
- return Either.FromLeft(errorValue);
- }
-
- // Skip if result is null
- if (branchResult.Right == null)
- {
- continue;
- }
- payload = branchResult.Right;
- }
-
- // Execute workflow features (Group, WithContext, Detach, Parallel, etc.)
- var featureExecutorFactory = new FeatureExecutorFactory();
- foreach (var feature in _features)
- {
- // Skip null features
- if (feature == null)
- {
- continue;
- }
-
- // Execute the feature
- var featureResult = await featureExecutorFactory.ExecuteFeature(feature, payload, cancellationToken);
- if (featureResult == null)
- {
- continue;
- }
-
- if (featureResult.IsLeft)
- {
- var errorValue = featureResult.Left;
- // Skip if error value is null
- if (errorValue == null)
- {
- continue;
- }
- return Either.FromLeft(errorValue);
- }
-
- if (feature.ShouldMerge)
- {
- // Skip if result is null
- if (featureResult.Right == null)
- {
- continue;
- }
- payload = featureResult.Right;
- }
- }
-
- // Create success result
- var success = _resultSelector(payload);
-
- // Skip if success result is null
- if (success == null)
- {
- // Return an empty success result
- return Either.FromRight(default!);
- }
-
- return Either.FromRight(success);
- }
- finally
- {
- // Execute finally activities
- foreach (var finallyActivity in _finallyActivities)
- {
- // Skip null finally activities
- if (finallyActivity == null)
- {
- continue;
- }
-
- // Skip if payload is null
- if (payload == null)
- {
- continue;
- }
-
- // Ignore errors from finally activities
- _ = await finallyActivity.Execute(payload, cancellationToken);
- }
- }
- }
- );
+ return new(ExecuteWorkflowAsync);
}
- // Dynamic helper to handle branches with different local payload types
- private async Task> ExecuteBranchWithLocalPayloadDynamic(
- object branchObj,
- TPayload payload,
+ ///
+ /// Executes the workflow: runs validations, activities, conditional logic, branches, features, and finally actions.
+ ///
+ /// The workflow request input.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if any stage fails, or the final success result (Right) on completion.
+ ///
+ private async Task> ExecuteWorkflowAsync(
+ TRequest request,
CancellationToken cancellationToken)
{
- // Skip if branch object is null
- if (branchObj == null)
+ var validationResult = await RunValidationsAsync(request, cancellationToken);
+ if (validationResult.IsLeft)
+ return Either.FromLeft(validationResult.Left!);
+
+ var payload = _contextFactory(request);
+ if (payload == null)
+ return Either.FromRight(_resultSelector(default!));
+
+ try
{
- return Either.FromRight(payload);
- }
+ var activitiesResult = await RunActivitiesAsync(payload, cancellationToken);
+ if (activitiesResult.IsLeft)
+ return Either.FromLeft(activitiesResult.Left!);
+
+ payload = activitiesResult.Right!;
- // Use reflection to call the appropriate generic method
- var branchType = branchObj.GetType();
+ var conditionalResult = await RunConditionalActivitiesAsync(payload, cancellationToken);
+ if (conditionalResult.IsLeft)
+ return Either.FromLeft(conditionalResult.Left!);
- // Skip if branch type is null
- if (branchType == null)
+ payload = conditionalResult.Right!;
+
+ var branchesResult = await RunBranchesAsync(payload, cancellationToken);
+ if (branchesResult.IsLeft)
+ return Either.FromLeft(branchesResult.Left!);
+
+ payload = branchesResult.Right!;
+
+ var branchLocalsResult = await RunBranchesWithLocalPayloadAsync(payload, cancellationToken);
+ if (branchLocalsResult.IsLeft)
+ return Either.FromLeft(branchLocalsResult.Left!);
+
+ payload = branchLocalsResult.Right!;
+
+ var featuresResult = await RunFeaturesAsync(payload, cancellationToken);
+ if (featuresResult.IsLeft)
+ return Either.FromLeft(featuresResult.Left!);
+
+ payload = featuresResult.Right!;
+
+ var successValue = _resultSelector(payload);
+ return Either.FromRight(successValue ?? default!);
+ }
+ finally
{
- return Either.FromRight(payload);
+ _ = await RunFinallyActivitiesAsync(payload, cancellationToken);
}
+ }
- try
+ ///
+ /// Runs all configured validations against the request.
+ ///
+ /// The workflow request.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either with Left if any validation fails, or Right with a placeholder payload on success.
+ ///
+ private async Task> RunValidationsAsync(
+ TRequest request,
+ CancellationToken cancellationToken)
+ {
+ foreach (var validation in _validations)
{
- if (branchType.IsGenericType &&
- branchType.GetGenericTypeDefinition() == typeof(BranchWithLocalPayload<,,>))
- {
- var typeArgs = branchType.GetGenericArguments();
- if (typeArgs == null || typeArgs.Length < 2)
- {
- return Either.FromRight(payload);
- }
-
- var localPayloadType = typeArgs[1];
- if (localPayloadType == null)
- {
- return Either.FromRight(payload);
- }
-
- // Get the generic method and make it specific to the local payload type
- var method = GetType().GetMethod(nameof(ExecuteBranchWithLocalPayload),
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- // Check if method is null before using it
- if (method == null)
- {
- throw new InvalidOperationException($"Method {nameof(ExecuteBranchWithLocalPayload)} not found.");
- }
-
- var genericMethod = method.MakeGenericMethod(localPayloadType);
- if (genericMethod == null)
- {
- return Either.FromRight(payload);
- }
-
- // Ensure payload is not null before passing to the method
- if (payload == null)
- {
- payload = default!; // Use default value if null
- }
-
- // Invoke the method with the right generic parameter
- var result = genericMethod.Invoke(this, new object[] { branchObj, payload, cancellationToken });
- return result == null
- ? throw new InvalidOperationException("Method invocation returned null.")
- : await (Task>)result;
- }
+ var validationOption = await validation.Validate(request, cancellationToken);
+ if (validationOption.IsSome && validationOption.Value != null)
+ return Either.FromLeft(validationOption.Value);
}
- catch (Exception)
+
+ return Either.FromRight(default!);
+ }
+
+ ///
+ /// Executes all registered activities in sequence, returning either the first encountered error or the transformed payload.
+ ///
+ /// The initial payload to process.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if any activity fails, or the final payload (Right) on success.
+ ///
+ private async Task> RunActivitiesAsync(
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ foreach (var activity in _activities)
{
- // If any reflection-related exception occurs, return the payload unchanged
- return Either.FromRight(payload);
+ var result = await activity.Execute(payload, cancellationToken);
+ if (result.IsLeft && result.Left != null)
+ return Either.FromLeft(result.Left);
+
+ payload = result.Right!;
}
- // If branch type isn't recognized, just return the payload unchanged
return Either.FromRight(payload);
}
- // Helper method to execute a branch with local payload
- private async Task> ExecuteBranchWithLocalPayload(
- BranchWithLocalPayload branch,
+ ///
+ /// Executes conditional activities when their condition is met.
+ ///
+ /// The current payload.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if any conditional activity fails, or the updated payload (Right) on success.
+ ///
+ private async Task> RunConditionalActivitiesAsync(
TPayload payload,
CancellationToken cancellationToken)
{
- // Check if branch is null
- if (branch == null)
+ foreach (var conditionalActivity in _conditionalActivities)
{
- return Either.FromRight(payload);
- }
+ if (!conditionalActivity.ShouldExecute(payload))
+ continue;
- // Check if condition is null
- if (branch.Condition == null)
- {
- return Either.FromRight(payload);
+ var result = await conditionalActivity.Activity.Execute(payload, cancellationToken);
+ if (result.IsLeft && result.Left != null)
+ return Either.FromLeft(result.Left);
+
+ payload = result.Right!;
}
- if (!branch.Condition(payload))
+ return Either.FromRight(payload);
+ }
+
+ ///
+ /// Executes simple branches when their condition is met.
+ ///
+ /// The current payload.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if any branch activity fails, or the updated payload (Right) on success.
+ ///
+ private async Task> RunBranchesAsync(
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ foreach (var branch in _branches)
{
- return Either.FromRight(payload);
+ if (!branch.Condition(payload))
+ continue;
+
+ foreach (var activity in branch.Activities)
+ {
+ var result = await activity.Execute(payload, cancellationToken);
+ if (result.IsLeft && result.Left != null)
+ return Either.FromLeft(result.Left);
+
+ payload = result.Right!;
+ }
}
- // Check if local payload factory is null
- if (branch.LocalPayloadFactory == null)
+ return Either.FromRight(payload);
+ }
+
+ ///
+ /// Executes branches with the local payload via reflection helper.
+ ///
+ /// The current payload.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if any branch fails, or the updated payload (Right) on success.
+ ///
+ private async Task> RunBranchesWithLocalPayloadAsync(
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ foreach (var branchObject in _branchesWithLocalPayload)
{
- return Either.FromRight(payload);
+ var result = await ExecuteBranchWithLocalPayloadDynamic(branchObject, payload, cancellationToken);
+ if (result.IsLeft && result.Left != null)
+ return Either.FromLeft(result.Left);
+
+ payload = result.Right!;
}
- // Create the local payload
- TLocalPayload? localPayload;
- try
+ return Either.FromRight(payload);
+ }
+
+ ///
+ /// Executes workflow features like Group, Context, Detach, Parallel, merging results when applicable.
+ ///
+ /// The current payload.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if any feature fails, or the updated payload (Right) on success.
+ ///
+ private async Task> RunFeaturesAsync(
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ var factory = new FeatureExecutorFactory();
+
+ foreach (var feature in _features)
{
- localPayload = branch.LocalPayloadFactory(payload);
+ var result = await factory.ExecuteFeature(feature, payload, cancellationToken);
+ if (result.IsLeft && result.Left != null)
+ return Either.FromLeft(result.Left);
+
+ if (feature.ShouldMerge)
+ payload = result.Right!;
}
- catch (Exception)
+
+ return Either.FromRight(payload);
+ }
+
+ ///
+ /// Executes all the "finally" activities, ignoring errors.
+ ///
+ /// The current payload.
+ /// Token to observe for cancellation.
+ /// The original payload after the "finally" activities.
+ private async Task RunFinallyActivitiesAsync(
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ foreach (var finallyActivity in _finallyActivities)
+ _ = await finallyActivity.Execute(payload, cancellationToken);
+
+ return payload;
+ }
+
+ ///
+ /// Dynamically executes a BranchWithLocalPayload via reflection, invoking the generic helper for the correct local payload type.
+ ///
+ ///
+ /// The branch instance, expected to be BranchWithLocalPayload<TPayload, TLocalPayload, TError>
+ ///
+ /// The main workflow payload.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if execution failed,
+ /// or the updated payload (Right) on success.
+ ///
+ private async Task> ExecuteBranchWithLocalPayloadDynamic(
+ object branchObject,
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ var branchType = branchObject.GetType();
+
+ if (branchType.IsGenericType &&
+ branchType.GetGenericTypeDefinition() == typeof(BranchWithLocalPayload<,,>))
{
- // If we can't create the local payload, return the payload unchanged
- return Either.FromRight(payload);
+ var methodInfo = typeof(WorkflowBuilder)
+ .GetMethod(nameof(ExecuteBranchWithLocalPayload), BindingFlags.NonPublic | BindingFlags.Instance);
+ if (methodInfo == null)
+ throw new InvalidOperationException($"Method {nameof(ExecuteBranchWithLocalPayload)} not found.");
+
+ var localPayloadType = branchType.GetGenericArguments()[1];
+ var genericMethod = methodInfo.MakeGenericMethod(localPayloadType);
+ var task = (Task>)genericMethod.Invoke(
+ this,
+ [
+ branchObject, payload, cancellationToken
+ ]
+ )!;
+ return await task.ConfigureAwait(false);
}
- // Check if activities collection is null
- if (branch.Activities == null)
- {
+ return Either.FromRight(payload);
+ }
+
+ ///
+ /// Executes a BranchWithLocalPayload<TPayload, TLocalPayload, TError> branch.
+ ///
+ /// Type of the local payload used by this branch.
+ /// The branch configuration and activities.
+ /// The main workflow payload.
+ /// Token to observe for cancellation.
+ ///
+ /// An Either containing the error (Left) if an activity fails,
+ /// or the updated payload (Right) on success.
+ ///
+ private async Task> ExecuteBranchWithLocalPayload(
+ BranchWithLocalPayload branch,
+ TPayload payload,
+ CancellationToken cancellationToken)
+ {
+ if (!branch.Condition(payload))
return Either.FromRight(payload);
- }
- // Execute the branch activities
+ var localPayload = branch.LocalPayloadFactory(payload);
+
foreach (var activity in branch.Activities)
{
- // Skip null activities
- if (activity == null)
- {
- continue;
- }
-
- var activityResult = await activity.Execute(payload, localPayload, cancellationToken);
- if (activityResult == null)
- {
- // Skip if the activity result is null
- continue;
- }
-
- if (activityResult.IsLeft)
- {
- return Either.FromLeft(activityResult.Left);
- }
+ var result = await activity.Execute(payload, localPayload, cancellationToken);
+ if (result.IsLeft)
+ return Either.FromLeft(result.Left);
- // Update both payloads
- if (activityResult.Right.Item1 != null)
- {
- payload = activityResult.Right.Item1;
- }
- if (activityResult.Right.Item2 != null)
- {
- localPayload = activityResult.Right.Item2;
- }
+ (payload, localPayload) = result.Right;
}
return Either.FromRight(payload);
}
-}
+}
\ No newline at end of file
diff --git a/Zooper.Bee/WorkflowExtensions.cs b/Zooper.Bee/WorkflowExtensions.cs
deleted file mode 100644
index 091effa..0000000
--- a/Zooper.Bee/WorkflowExtensions.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Zooper.Fox;
-
-namespace Zooper.Bee;
-
-///
-/// Extension methods for the Workflow class.
-///
-public static class WorkflowExtensions
-{
- ///
- /// Executes a workflow that doesn't require a request parameter.
- ///
- /// The type of the success result
- /// The type of the error result
- /// The workflow to execute
- /// The result of the workflow execution
- public static Task> Execute(
- this Workflow workflow)
- {
- return workflow.Execute(Unit.Value);
- }
-
- ///
- /// Executes a workflow that doesn't require a request parameter.
- ///
- /// The type of the success result
- /// The type of the error result
- /// The workflow to execute
- /// A cancellation token to observe while waiting for the task to complete
- /// The result of the workflow execution
- public static Task> Execute(
- this Workflow workflow,
- CancellationToken cancellationToken)
- {
- return workflow.Execute(Unit.Value, cancellationToken);
- }
-}
\ No newline at end of file