From ac643c9762a6c1b35bd114b66d517f066f202c2e Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 24 Apr 2025 10:48:14 +0200 Subject: [PATCH 1/2] Readability improvements --- CHANGELOG.md | 6 + Directory.Build.props | 2 +- Zooper.Bee/WorkflowBuilder.cs | 755 ++++++++++++++-------------------- 3 files changed, 325 insertions(+), 438 deletions(-) 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/WorkflowBuilder.cs b/Zooper.Bee/WorkflowBuilder.cs index c6835f6..43c860e 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(this, branch); } /// @@ -301,7 +313,7 @@ public BranchWithLocalPayloadBuilder(_ => true, localPayloadFactory); _branchesWithLocalPayload.Add(branch); - return new BranchWithLocalPayloadBuilder(this, branch); + return new(this, 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 From 8c253252561851605d05dd97219f9c78e2f11973 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 24 Apr 2025 10:53:29 +0200 Subject: [PATCH 2/2] Some more improvements --- .../ParameterlessWorkflowExample.cs | 130 +++++++++++------- .../ParameterlessWorkflowTests.cs | 106 +++++++++----- Zooper.Bee/BranchWithLocalPayloadBuilder.cs | 35 +++-- Zooper.Bee/Extensions/WorkflowExtensions.cs | 30 ++++ Zooper.Bee/WorkflowBuilder.cs | 6 +- Zooper.Bee/WorkflowExtensions.cs | 39 ------ 6 files changed, 210 insertions(+), 136 deletions(-) delete mode 100644 Zooper.Bee/WorkflowExtensions.cs 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 43c860e..04fc8b6 100644 --- a/Zooper.Bee/WorkflowBuilder.cs +++ b/Zooper.Bee/WorkflowBuilder.cs @@ -276,7 +276,7 @@ public BranchWithLocalPayloadBuilder(condition, localPayloadFactory); _branchesWithLocalPayload.Add(branch); - return new(this, branch); + return new(branch); } /// @@ -295,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; } @@ -313,7 +313,7 @@ public BranchWithLocalPayloadBuilder(_ => true, localPayloadFactory); _branchesWithLocalPayload.Add(branch); - return new(this, branch); + return new(branch); } /// 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