diff --git a/.github/workflows/nuget.publish.yml b/.github/workflows/nuget.publish.yml index 7974401..9b78e65 100644 --- a/.github/workflows/nuget.publish.yml +++ b/.github/workflows/nuget.publish.yml @@ -4,8 +4,7 @@ on: push: tags: - "v*" - branches: - - develop + workflow_dispatch: permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cf8df..0e2a6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Renamed all `Workflow` classes to `Railway`** to better reflect the railway-oriented programming pattern + - `Workflow` -> `Railway` + - `WorkflowBuilder<...>` -> `RailwayBuilder<...>` + - `WorkflowBuilderFactory` -> `RailwayBuilderFactory` + - `CreateWorkflow<...>()` -> `CreateRailway<...>()` + - `IWorkflowStep` -> `IRailwayStep` + - `IWorkflowValidation` -> `IRailwayValidation` + - `IWorkflowGuard` -> `IRailwayGuard` + - `AddWorkflows()` -> `AddRailways()` + - `AddWorkflowSteps()` -> `AddRailwaySteps()` +- All old `Workflow` names are preserved as `[Obsolete]` shims for backward compatibility +- Updated all example files to use the new Railway terminology + ### Added - New `IWorkflowStep` and `IWorkflowStep` interfaces to replace `IWorkflowActivity` diff --git a/Directory.Build.props b/Directory.Build.props index 748ecae..110d606 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,9 +8,9 @@ https://github.com/zooper-lib/Bee git icon.png - workflow;builder;pipeline;functional;either;C# + railway;workflow;builder;pipeline;functional;either;C# README.md - A .NET library for building robust, functional workflows and processing pipelines. + A .NET library for building robust, functional railways (processing pipelines) using railway-oriented programming. 3.4.0 diff --git a/README.md b/README.md index dcb96ce..59a2e38 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,22 @@ [![NuGet Version](https://img.shields.io/nuget/v/Zooper.Bee.svg)](https://www.nuget.org/packages/Zooper.Bee/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -A flexible and powerful workflow library for .NET that allows you to define complex business processes with a fluent +A flexible and powerful railway-oriented programming library for .NET that allows you to define complex business processes with a fluent API. ## Overview -Zooper.Bee lets you create workflows that process requests and produce either successful results or meaningful errors. -The library uses a builder pattern to construct workflows with various execution patterns including sequential, +Zooper.Bee lets you create railways that process requests and produce either successful results or meaningful errors. +The library uses a builder pattern to construct railways with various execution patterns including sequential, conditional, parallel, and detached operations. ## Key Concepts -- **Workflow**: A sequence of operations that process a request to produce a result or error -- **Request**: The input data to the workflow -- **Payload**: Data that passes through and gets modified by workflow activities -- **Success**: The successful result of the workflow -- **Error**: The errors result if the workflow fails +- **Railway**: A sequence of operations that process a request to produce a result or error +- **Request**: The input data to the railway +- **Payload**: Data that passes through and gets modified by railway activities +- **Success**: The successful result of the railway +- **Error**: The errors result if the railway fails ## Installation @@ -31,8 +31,8 @@ dotnet add package Zooper.Bee ## Getting Started ```csharp -// Define a simple workflow -var workflow = new WorkflowBuilder( +// Define a simple railway +var railway = new RailwayBuilder( // Factory function that creates the initial payload from the request request => new Payload { Data = request.Data }, @@ -55,8 +55,8 @@ var workflow = new WorkflowBuilder }) .Build(); -// Execute the workflow -var result = await workflow.Execute(new Request { Data = "hello world" }, CancellationToken.None); +// Execute the railway +var result = await railway.Execute(new Request { Data = "hello world" }, CancellationToken.None); if (result.IsRight) { Console.WriteLine($"Success: {result.Right.ProcessedData}"); // Output: Success: HELLO WORLD @@ -67,7 +67,7 @@ else } ``` -## Building Workflows +## Building Railways ### Validation @@ -91,8 +91,8 @@ Validates the incoming request before processing begins. ### Guards -Guards allow you to define checks that run before a workflow begins execution. They're ideal for authentication, -authorization, account validation, or any other requirement that must be satisfied before a workflow can proceed. +Guards allow you to define checks that run before a railway begins execution. They're ideal for authentication, +authorization, account validation, or any other requirement that must be satisfied before a railway can proceed. ```csharp // Asynchronous guard @@ -112,14 +112,14 @@ authorization, account validation, or any other requirement that must be satisfi #### Benefits of Guards -- Guards run before creating the workflow context, providing early validation -- They provide a clear separation between "can this workflow run?" and the actual workflow logic +- Guards run before creating the railway context, providing early validation +- They provide a clear separation between "can this railway run?" and the actual railway logic - Common checks like authentication can be standardized and reused -- Failures short-circuit the workflow, preventing unnecessary work +- Failures short-circuit the railway, preventing unnecessary work ### Activities -Activities are the building blocks of a workflow. They process the payload and can produce either a success (with +Activities are the building blocks of a railway. They process the payload and can produce either a success (with the modified payload) or an error. ```csharp @@ -164,7 +164,7 @@ Activities that only execute if a condition is met. ### Groups Organize related activities into logical groups. Groups can have conditions and always merge their results back to the -main workflow. +main railway. ```csharp .Group( @@ -219,7 +219,7 @@ Execute multiple groups of activities in parallel and merge the results. ### Detached Execution Execute activities in the background without waiting for their completion. Results from detached activities are not -merged back into the main workflow. +merged back into the main railway. ```csharp .Detach( @@ -253,7 +253,7 @@ Execute multiple groups of detached activities in parallel without waiting for c ### Finally Block -Activities that always execute, even if the workflow fails. +Activities that always execute, even if the railway fails. ```csharp .Finally(payload => @@ -285,7 +285,7 @@ Activities that always execute, even if the workflow fails. ### Conditional Branching -Use conditions to determine which path to take in a workflow. +Use conditions to determine which path to take in a railway. ```csharp .Group( @@ -302,53 +302,83 @@ Use conditions to determine which path to take in a workflow. ## Dependency Injection Integration -Zooper.Bee integrates seamlessly with .NET's dependency injection system. You can register all workflow components with +Zooper.Bee integrates seamlessly with .NET's dependency injection system. You can register all railway components with a single extension method: ```csharp // In Startup.cs or Program.cs -services.AddWorkflows(); +services.AddRailways(); ``` This will scan all assemblies and register: -- All workflow validations -- All workflow activities -- All concrete workflow classes (classes ending with "Workflow") +- All railway validations +- All railway activities +- All concrete railway classes (classes ending with "Railway") You can also register specific components: ```csharp // Register only validations -services.AddWorkflowValidations(); +services.AddRailwayValidations(); // Register only activities -services.AddWorkflowActivities(); +services.AddRailwayActivities(); // Specify which assemblies to scan -services.AddWorkflows(new[] { typeof(Program).Assembly }); +services.AddRailways(new[] { typeof(Program).Assembly }); // Specify service lifetime (Singleton, Scoped, Transient) -services.AddWorkflows(lifetime: ServiceLifetime.Singleton); +services.AddRailways(lifetime: ServiceLifetime.Singleton); ``` ## Performance Considerations - Use `Parallel` for CPU-bound operations that can benefit from parallel execution -- Use `Detach` for I/O operations that don't affect the main workflow +- Use `Detach` for I/O operations that don't affect the main railway - Be mindful of resource contention in parallel operations - Consider using `WithContext` to maintain state between related activities ## Best Practices 1. Keep activities small and focused on a single responsibility -2. Use descriptive names for your workflow methods +2. Use descriptive names for your railway methods 3. Group related activities together 4. Handle errors at appropriate levels 5. Use `Finally` for cleanup operations 6. Validate requests early to fail fast 7. Use contextual state to avoid passing too many parameters +## Migration from Workflow to Railway + +As of the latest version, all `Workflow` classes have been renamed to `Railway` to better reflect the railway-oriented programming pattern used by the library. The old `Workflow` names are preserved as `[Obsolete]` shims for backward compatibility. + +### What changed + +| Old Name | New Name | +|---|---| +| `Workflow` | `Railway` | +| `WorkflowBuilder<...>` | `RailwayBuilder<...>` | +| `WorkflowBuilderFactory` | `RailwayBuilderFactory` | +| `CreateWorkflow<...>()` | `CreateRailway<...>()` | +| `IWorkflowStep` | `IRailwayStep` | +| `IWorkflowValidation` | `IRailwayValidation` | +| `IWorkflowGuard` | `IRailwayGuard` | +| `AddWorkflows()` | `AddRailways()` | +| `AddWorkflowSteps()` | `AddRailwaySteps()` | + +### Backward compatibility + +All old type names and extension methods are still available but marked with `[Obsolete]`. Your existing code will continue to compile and work, but you will see deprecation warnings encouraging you to migrate to the new names. + +### How to migrate + +1. Replace all `Workflow<` type references with `Railway<` +2. Replace `WorkflowBuilder<` with `RailwayBuilder<` +3. Replace `WorkflowBuilderFactory.CreateWorkflow<` with `RailwayBuilderFactory.CreateRailway<` +4. Replace DI registration calls (`AddWorkflows()` -> `AddRailways()`, etc.) +5. Update any interface implementations (`IWorkflowStep` -> `IRailwayStep`, etc.) + ## License MIT License (Copyright details here) \ No newline at end of file diff --git a/Zooper.Bee.Example/BranchingExample.cs b/Zooper.Bee.Example/BranchingExample.cs index 321f7e7..6e4b144 100644 --- a/Zooper.Bee.Example/BranchingExample.cs +++ b/Zooper.Bee.Example/BranchingExample.cs @@ -30,15 +30,15 @@ public record RegistrationPayload( public static async Task RunExample() { - Console.WriteLine("\n=== Workflow Grouping Example ===\n"); + Console.WriteLine("\n=== Railway Grouping Example ===\n"); // Create sample requests var standardUserRequest = new RegistrationRequest("user@example.com", "Password123!", false); var vipUserRequest = new RegistrationRequest("vip@example.com", "VIPPassword123!", true); var invalidEmailRequest = new RegistrationRequest("invalid-email", "Password123!", false); - // Build the registration workflow - var workflow = CreateRegistrationWorkflow(); + // Build the registration railway + var workflow = CreateRegistrationRailway(); // Process standard user registration Console.WriteLine("Registering standard user:"); @@ -58,7 +58,7 @@ public static async Task RunExample() } private static async Task ProcessRegistration( - Workflow workflow, + Railway workflow, RegistrationRequest request) { var result = await workflow.Execute(request); @@ -82,9 +82,9 @@ private static async Task ProcessRegistration( } } - private static Workflow CreateRegistrationWorkflow() + private static Railway CreateRegistrationRailway() { - return new WorkflowBuilder( + return new RailwayBuilder( // Create initial payload from request request => new RegistrationPayload( Guid.NewGuid(), // Generate a new unique ID diff --git a/Zooper.Bee.Example/ContextLocalPayloadExample.cs b/Zooper.Bee.Example/ContextLocalPayloadExample.cs index e65e054..870718c 100644 --- a/Zooper.Bee.Example/ContextLocalPayloadExample.cs +++ b/Zooper.Bee.Example/ContextLocalPayloadExample.cs @@ -21,7 +21,7 @@ public record OrderConfirmation( // Error model public record OrderError(string Code, string Message); - // Main workflow payload model + // Main railway payload model public record OrderPayload( int OrderId, string CustomerName, @@ -40,14 +40,14 @@ public record ShippingPayload( public static async Task RunExample() { - Console.WriteLine("\n=== Workflow With Context Local Payload Example ===\n"); + Console.WriteLine("\n=== Railway With Context Local Payload Example ===\n"); // Create sample requests var standardOrder = new OrderRequest(2001, "Alice Johnson", 75.00m, false); var shippingOrder = new OrderRequest(2002, "Bob Smith", 120.00m, true); - // Build the order processing workflow - var workflow = CreateOrderWorkflow(); + // Build the order processing railway + var workflow = CreateOrderRailway(); // Process the standard order (no shipping) Console.WriteLine("Processing standard order (no shipping):"); @@ -61,7 +61,7 @@ public static async Task RunExample() } private static async Task ProcessOrder( - Workflow workflow, + Railway workflow, OrderRequest request) { var result = await workflow.Execute(request); @@ -89,9 +89,9 @@ private static async Task ProcessOrder( } } - private static Workflow CreateOrderWorkflow() + private static Railway CreateOrderRailway() { - return new WorkflowBuilder( + return new RailwayBuilder( // Create initial payload from request request => new OrderPayload( request.OrderId, diff --git a/Zooper.Bee.Example/ParallelExecutionExample.cs b/Zooper.Bee.Example/ParallelExecutionExample.cs index 07fa77d..67a2751 100644 --- a/Zooper.Bee.Example/ParallelExecutionExample.cs +++ b/Zooper.Bee.Example/ParallelExecutionExample.cs @@ -33,9 +33,9 @@ public static async Task RunExample() true ); - // Build the workflows - var parallelWorkflow = CreateParallelWorkflow(); - var parallelDetachedWorkflow = CreateParallelDetachedWorkflow(); + // Build the railways + var parallelWorkflow = CreateParallelRailway(); + var parallelDetachedWorkflow = CreateParallelDetachedRailway(); // Process with parallel execution Console.WriteLine("Processing with parallel execution:"); @@ -49,7 +49,7 @@ public static async Task RunExample() } private static async Task ProcessData( - Workflow workflow, + Railway workflow, DataProcessingRequest request) { var result = await workflow.Execute(request); @@ -68,9 +68,9 @@ private static async Task ProcessData( } } - private static Workflow CreateParallelWorkflow() + private static Railway CreateParallelRailway() { - return new WorkflowBuilder( + return new RailwayBuilder( // Create initial payload from request request => new DataProcessingPayload( request.DataId, @@ -151,9 +151,9 @@ private static Workflow CreateParallelDetachedWorkflow() + private static Railway CreateParallelDetachedRailway() { - return new WorkflowBuilder( + return new RailwayBuilder( // Create initial payload from request request => new DataProcessingPayload( request.DataId, @@ -171,7 +171,7 @@ private static Workflow.FromRight( payload with { @@ -213,7 +213,7 @@ payload with // Finalize the processing (this runs immediately, not waiting for detached tasks) .Do(payload => { - Console.WriteLine($"Main workflow: Finalizing data processing for {payload.DataId}..."); + Console.WriteLine($"Main railway: Finalizing data processing for {payload.DataId}..."); var completedAt = DateTime.UtcNow; return Either.FromRight( diff --git a/Zooper.Bee.Example/ParameterlessWorkflowExample.cs b/Zooper.Bee.Example/ParameterlessRailwayExample.cs similarity index 75% rename from Zooper.Bee.Example/ParameterlessWorkflowExample.cs rename to Zooper.Bee.Example/ParameterlessRailwayExample.cs index 5dc748c..5589398 100644 --- a/Zooper.Bee.Example/ParameterlessWorkflowExample.cs +++ b/Zooper.Bee.Example/ParameterlessRailwayExample.cs @@ -5,7 +5,7 @@ namespace Zooper.Bee.Example; -public sealed class ParameterlessWorkflowExample +public sealed class ParameterlessRailwayExample { // Success model public record ProcessingResult(DateTime ProcessedAt, string Status); @@ -21,9 +21,9 @@ public record ProcessingPayload( public static async Task RunExample() { - Console.WriteLine("\n=== Parameterless Workflow Example ===\n"); + Console.WriteLine("\n=== Parameterless Railway Example ===\n"); - Console.WriteLine("Example 1: Using WorkflowBuilderFactory.Create"); + Console.WriteLine("Example 1: Using RailwayBuilderFactory.Create"); await RunExampleWithFactory(); Console.WriteLine("\nExample 2: Using Unit type directly"); @@ -35,15 +35,15 @@ public static async Task RunExample() private static async Task RunExampleWithFactory() { - // Create a workflow that doesn't need input parameters - var workflow = WorkflowBuilderFactory.CreateWorkflow( + // Create a railway that doesn't need input parameters + var workflow = RailwayBuilderFactory.CreateRailway( // Initial payload factory - no parameters needed () => new ProcessingPayload(StartedAt: DateTime.UtcNow), // Result selector - convert final payload to success result payload => new ProcessingResult(DateTime.UtcNow, payload.Status), - // Configure the workflow + // Configure the railway builder => builder .Do(payload => { @@ -75,19 +75,19 @@ payload with if (result.IsRight) { - Console.WriteLine($"Workflow completed successfully: {result.Right.Status}"); + Console.WriteLine($"Railway completed successfully: {result.Right.Status}"); Console.WriteLine($"Processed at: {result.Right.ProcessedAt}"); } else { - Console.WriteLine($"Workflow failed: [{result.Left.Code}] {result.Left.Message}"); + Console.WriteLine($"Railway failed: [{result.Left.Code}] {result.Left.Message}"); } } private static async Task RunExampleWithUnit() { - // Create a workflow with Unit type as request - var workflow = new WorkflowBuilder( + // Create a railway with Unit type as request + var workflow = new RailwayBuilder( // Use Unit parameter (ignored) _ => new ProcessingPayload(StartedAt: DateTime.UtcNow), @@ -124,19 +124,19 @@ payload with if (result.IsRight) { - Console.WriteLine($"Workflow completed successfully: {result.Right.Status}"); + Console.WriteLine($"Railway completed successfully: {result.Right.Status}"); Console.WriteLine($"Processed at: {result.Right.ProcessedAt}"); } else { - Console.WriteLine($"Workflow failed: [{result.Left.Code}] {result.Left.Message}"); + Console.WriteLine($"Railway failed: [{result.Left.Code}] {result.Left.Message}"); } } private static async Task RunExampleWithExtension() { - // Create a workflow with Unit type as request - var workflow = new WorkflowBuilder( + // Create a railway with Unit type as request + var workflow = new RailwayBuilder( // Use Unit parameter (ignored) _ => new ProcessingPayload(StartedAt: DateTime.UtcNow), @@ -173,12 +173,12 @@ payload with if (result.IsRight) { - Console.WriteLine($"Workflow completed successfully: {result.Right.Status}"); + Console.WriteLine($"Railway completed successfully: {result.Right.Status}"); Console.WriteLine($"Processed at: {result.Right.ProcessedAt}"); } else { - Console.WriteLine($"Workflow failed: [{result.Left.Code}] {result.Left.Message}"); + Console.WriteLine($"Railway failed: [{result.Left.Code}] {result.Left.Message}"); } } } \ No newline at end of file diff --git a/Zooper.Bee.Example/Program.cs b/Zooper.Bee.Example/Program.cs index 333c484..8b74cd1 100644 --- a/Zooper.Bee.Example/Program.cs +++ b/Zooper.Bee.Example/Program.cs @@ -13,7 +13,7 @@ public record OrderConfirmation(string ConfirmationNumber, DateTime ProcessedDat // Error model public record OrderError(string ErrorCode, string Message); - // Payload model to carry data through the workflow + // Payload model to carry data through the railway public record OrderProcessingPayload( int OrderId, string CustomerName, @@ -24,7 +24,7 @@ public record OrderProcessingPayload( public static async Task Main() { - Console.WriteLine("=== Zooper.Bee Workflow Example ===\n"); + Console.WriteLine("=== Zooper.Bee Railway Example ===\n"); // Create a valid order request var validOrder = new OrderRequest(1001, "John Doe", 99.99m); @@ -46,18 +46,18 @@ public static async Task Main() // Run the parallel execution example await ParallelExecutionExample.RunExample(); - // Run the parameterless workflow example - await ParameterlessWorkflowExample.RunExample(); + // Run the parameterless railway example + await ParameterlessRailwayExample.RunExample(); } private static async Task ProcessOrder(OrderRequest request) { Console.WriteLine($"Processing order {request.OrderId} for {request.CustomerName}..."); - // Create the workflow - var workflow = CreateOrderWorkflow(); + // Create the railway + var workflow = CreateOrderRailway(); - // Execute the workflow with the request + // Execute the railway with the request var result = await workflow.Execute(request); // Handle the result @@ -75,9 +75,9 @@ private static async Task ProcessOrder(OrderRequest request) } } - private static Workflow CreateOrderWorkflow() + private static Railway CreateOrderRailway() { - return new WorkflowBuilder( + return new RailwayBuilder( // Context factory: Create initial payload from request request => new OrderProcessingPayload( request.OrderId, diff --git a/Zooper.Bee.MediatR/RailwayHandler.cs b/Zooper.Bee.MediatR/RailwayHandler.cs new file mode 100644 index 0000000..1c53b19 --- /dev/null +++ b/Zooper.Bee.MediatR/RailwayHandler.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Zooper.Fox; + +// ReSharper disable IdentifierTypo + +namespace Zooper.Bee.MediatR; + +/// +/// Base class for railway handlers that process requests through MediatR +/// +/// The request type +/// The internal railway payload type +/// The success result type +/// The error result type +public abstract class RailwayHandler + : IRequestHandler> + where TRequest : IRequest> +{ + /// + /// Gets the factory function to create the initial payload from the request. + /// + protected abstract Func PayloadFactory { get; } + + /// + /// Gets the selector function to create the success result from the final payload. + /// + protected abstract Func ResultSelector { get; } + + /// + /// Configures the railway using the provided builder. + /// + /// The railway builder to configure + protected abstract void ConfigureRailway(RailwayBuilder builder); + + /// + /// Handles the request and returns the result. + /// + /// The request to handle + /// The cancellation token + /// Either an error or success result + public async Task> Handle( + TRequest request, + CancellationToken cancellationToken) + { + var railway = CreateRailway(); + return await railway.Execute(request, cancellationToken); + } + + /// + /// Creates the railway instance. + /// + /// The configured railway + // ReSharper disable once MemberCanBePrivate.Global + protected Railway CreateRailway() + { + var builder = new RailwayBuilder( + PayloadFactory, + ResultSelector + ); + + ConfigureRailway(builder); + + return builder.Build(); + } +} diff --git a/Zooper.Bee.MediatR/WorkflowHandler.cs b/Zooper.Bee.MediatR/WorkflowHandler.cs index 7ca2835..957601e 100644 --- a/Zooper.Bee.MediatR/WorkflowHandler.cs +++ b/Zooper.Bee.MediatR/WorkflowHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using MediatR; @@ -15,6 +15,7 @@ namespace Zooper.Bee.MediatR; /// The internal workflow payload type /// The success result type /// The error result type +[Obsolete("Use RailwayHandler instead. This class will be removed in a future version.")] public abstract class WorkflowHandler : IRequestHandler> where TRequest : IRequest> @@ -33,7 +34,8 @@ public abstract class WorkflowHandler /// Configures the workflow using the provided builder. /// /// The workflow builder to configure - protected abstract void ConfigureWorkflow(WorkflowBuilder builder); + [Obsolete("Use ConfigureRailway on RailwayHandler instead. This method will be removed in a future version.")] + protected abstract void ConfigureWorkflow(RailwayBuilder builder); /// /// Handles the request and returns the result. @@ -45,18 +47,19 @@ public async Task> Handle( TRequest request, CancellationToken cancellationToken) { - var workflow = CreateWorkflow(); - return await workflow.Execute(request, cancellationToken); + var railway = CreateWorkflow(); + return await railway.Execute(request, cancellationToken); } /// /// Creates the workflow instance. /// - /// The configured workflow + /// The configured railway // ReSharper disable once MemberCanBePrivate.Global - protected Workflow CreateWorkflow() + [Obsolete("Use CreateRailway on RailwayHandler instead. This method will be removed in a future version.")] + protected Railway CreateWorkflow() { - var builder = new WorkflowBuilder( + var builder = new RailwayBuilder( PayloadFactory, ResultSelector ); @@ -65,4 +68,4 @@ protected Workflow CreateWorkflow() return builder.Build(); } -} \ No newline at end of file +} diff --git a/Zooper.Bee.Tests/BranchTests.cs b/Zooper.Bee.Tests/BranchTests.cs index 406f65a..1273ee6 100644 --- a/Zooper.Bee.Tests/BranchTests.cs +++ b/Zooper.Bee.Tests/BranchTests.cs @@ -31,7 +31,7 @@ private record TestError(string Code, string Message); public async Task Branch_ExecutesWhenConditionIsTrue() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value, request.Category), payload => new TestSuccess(payload.Name, payload.ProcessingResult ?? "Not processed") ) @@ -85,7 +85,7 @@ public async Task Branch_ExecutesWhenConditionIsTrue() public async Task Branch_SkipsWhenConditionIsFalse() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value, request.Category), payload => new TestSuccess(payload.Name, payload.ProcessingResult ?? "Not processed") ) @@ -117,7 +117,7 @@ payload with public async Task Branch_UnconditionalBranch_AlwaysExecutes() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value, request.Category), payload => new TestSuccess(payload.Name, payload.ProcessingResult ?? "Not processed") ) @@ -142,7 +142,7 @@ public async Task Branch_UnconditionalBranch_AlwaysExecutes() public async Task Branch_MultipleBranches_CorrectlyExecutes() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value, request.Category), payload => new TestSuccess(payload.Name, payload.ProcessingResult ?? "Not processed") ) @@ -185,7 +185,7 @@ public async Task Branch_MultipleBranches_CorrectlyExecutes() public async Task Branch_WithError_StopsExecutionAndReturnsError() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value, request.Category), payload => new TestSuccess(payload.Name, payload.ProcessingResult ?? "Not processed") ) @@ -225,7 +225,7 @@ public async Task Branch_WithError_StopsExecutionAndReturnsError() public async Task Branch_WithMultipleActivities_ExecutesAllInOrder() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value, request.Category), payload => new TestSuccess(payload.Name, payload.ProcessingResult ?? "Not processed") ) diff --git a/Zooper.Bee.Tests/BranchWithLocalPayloadTests.cs b/Zooper.Bee.Tests/BranchWithLocalPayloadTests.cs index 122d59c..720591b 100644 --- a/Zooper.Bee.Tests/BranchWithLocalPayloadTests.cs +++ b/Zooper.Bee.Tests/BranchWithLocalPayloadTests.cs @@ -12,7 +12,7 @@ public class ContextTests // Request model private record ProductRequest(int Id, string Name, decimal Price, bool NeedsCustomProcessing); - // Main workflow payload model + // Main railway payload model private record ProductPayload( int Id, string Name, @@ -42,7 +42,7 @@ private record ProductError(string Code, string Message); public async Task WithContext_ExecutesWhenConditionIsTrue() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( // Create the main payload from the request request => new ProductPayload( request.Id, @@ -122,7 +122,7 @@ public async Task WithContext_ExecutesWhenConditionIsTrue() public async Task WithContext_LocalPayloadIsolated_NotAffectedByOtherActivities() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, payload.ProcessingResult) @@ -204,7 +204,7 @@ public async Task WithContext_LocalPayloadIsolated_NotAffectedByOtherActivities( public async Task WithContext_ErrorInBranch_StopsExecutionAndReturnsError() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, payload.ProcessingResult) @@ -267,7 +267,7 @@ public async Task WithContext_ErrorInBranch_StopsExecutionAndReturnsError() public async Task WithContext_MultipleActivitiesInSameBranch_ShareLocalPayload() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, payload.ProcessingResult) @@ -358,7 +358,7 @@ public async Task WithContext_MultipleActivitiesInSameBranch_ShareLocalPayload() public async Task WithContext_UnconditionalBranch_AlwaysExecutes() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, payload.ProcessingResult) diff --git a/Zooper.Bee.Tests/DetachedExecutionTests.cs b/Zooper.Bee.Tests/DetachedExecutionTests.cs index 4446dfb..d518292 100644 --- a/Zooper.Bee.Tests/DetachedExecutionTests.cs +++ b/Zooper.Bee.Tests/DetachedExecutionTests.cs @@ -37,7 +37,7 @@ public async Task Detached_ExecutesInBackground_DoesNotAffectMainWorkflow() var syncObj = new object(); var backgroundTaskRan = false; - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new NotificationPayload(request.UserId, request.Message, request.IsUrgent), payload => new NotificationResult(payload.UserId, payload.Status) ) @@ -123,7 +123,7 @@ public async Task Detached_WithCondition_OnlyExecutesWhenConditionIsTrue() var urgentTaskRan = false; var regularTaskRan = false; - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new NotificationPayload(request.UserId, request.Message, request.IsUrgent), payload => new NotificationResult(payload.UserId, payload.Status) ) @@ -255,7 +255,7 @@ public async Task Detached_WithMultipleActivities_ExecutesAllInOrder() var syncObj = new object(); var executionOrder = new List(); - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new NotificationPayload(request.UserId, request.Message, request.IsUrgent), payload => new NotificationResult(payload.UserId, payload.Status) ) diff --git a/Zooper.Bee.Tests/ParallelExecutionTests.cs b/Zooper.Bee.Tests/ParallelExecutionTests.cs index 84ccd48..626d837 100644 --- a/Zooper.Bee.Tests/ParallelExecutionTests.cs +++ b/Zooper.Bee.Tests/ParallelExecutionTests.cs @@ -32,7 +32,7 @@ private record TestError(string Code, string Message); public async Task Parallel_ExecutesGroupsInParallel_CombinesResults() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Id, request.Values), payload => new TestSuccess(payload.Id, payload.Sum, payload.Product, payload.IsProcessed) ) @@ -87,7 +87,7 @@ public async Task Parallel_ExecutesGroupsInParallel_CombinesResults() public async Task Parallel_WithConditionalGroups_OnlyExecutesMatchingGroups() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Id, request.Values), payload => new TestSuccess(payload.Id, payload.Sum, payload.Product, payload.IsProcessed) ) @@ -164,7 +164,7 @@ public async Task Parallel_WithConditionalGroups_OnlyExecutesMatchingGroups() public async Task Parallel_ErrorInOneGroup_StopsExecutionAndReturnsError() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Id, request.Values), payload => new TestSuccess(payload.Id, payload.Sum, payload.Product, payload.IsProcessed) ) @@ -235,7 +235,7 @@ public async Task ParallelDetached_DetachedGroupsDoNotAffectResult() var syncObj = new object(); var backgroundTaskRan = false; - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Id, request.Values), payload => new TestSuccess(payload.Id, payload.Sum, payload.Product, payload.IsProcessed) ) @@ -326,7 +326,7 @@ public async Task ParallelDetached_ErrorInDetachedGroup_DoesNotAffectMainWorkflo var syncObj = new object(); var backgroundTaskRan = false; - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Id, request.Values), payload => new TestSuccess(payload.Id, payload.Sum, payload.Product, payload.IsProcessed) ) diff --git a/Zooper.Bee.Tests/ParameterlessWorkflowTests.cs b/Zooper.Bee.Tests/ParameterlessRailwayTests.cs similarity index 81% rename from Zooper.Bee.Tests/ParameterlessWorkflowTests.cs rename to Zooper.Bee.Tests/ParameterlessRailwayTests.cs index e8f6a1d..ac3b92c 100644 --- a/Zooper.Bee.Tests/ParameterlessWorkflowTests.cs +++ b/Zooper.Bee.Tests/ParameterlessRailwayTests.cs @@ -7,7 +7,7 @@ namespace Zooper.Bee.Tests; -public class ParameterlessWorkflowTests +public class ParameterlessRailwayTests { #region Test Models @@ -23,10 +23,10 @@ private record TestError(string Code, string Message); #endregion [Fact] - public async Task ParameterlessWorkflow_UsingUnitType_CanBeExecuted() + public async Task ParameterlessRailway_UsingUnitType_CanBeExecuted() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( // Convert Unit to initial payload _ => new TestPayload(DateTime.UtcNow), @@ -59,17 +59,17 @@ payload with } [Fact] - public async Task ParameterlessWorkflow_UsingFactory_CanBeExecuted() + public async Task ParameterlessRailway_UsingFactory_CanBeExecuted() { // Arrange - var workflow = WorkflowBuilderFactory.CreateWorkflow( + var workflow = RailwayBuilderFactory.CreateRailway( // Initial payload factory () => new TestPayload(DateTime.UtcNow), // Result selector payload => new TestSuccess(payload.Status, true), - // Configure the workflow + // Configure the railway builder => builder .Do(payload => Either.FromRight( payload with @@ -97,10 +97,10 @@ payload with } [Fact] - public async Task ParameterlessWorkflow_UsingExtensionMethod_CanBeExecuted() + public async Task ParameterlessRailway_UsingExtensionMethod_CanBeExecuted() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( _ => new TestPayload(DateTime.UtcNow), payload => new TestSuccess(payload.Status, true) ) @@ -130,10 +130,10 @@ payload with } [Fact] - public async Task ParameterlessWorkflow_WithError_ReturnsError() + public async Task ParameterlessRailway_WithError_ReturnsError() { // Arrange - var workflow = WorkflowBuilderFactory.Create( + var workflow = RailwayBuilderFactory.Create( () => new TestPayload(DateTime.UtcNow), payload => new TestSuccess(payload.Status, true) ) @@ -146,7 +146,7 @@ payload with ) .Do(payload => { - // Simulate an error in the workflow + // Simulate an error in the railway return Either.FromLeft( new TestError("PROCESSING_FAILED", "Failed to complete processing") ); diff --git a/Zooper.Bee.Tests/WorkflowInternalsTests.cs b/Zooper.Bee.Tests/RailwayInternalsTests.cs similarity index 92% rename from Zooper.Bee.Tests/WorkflowInternalsTests.cs rename to Zooper.Bee.Tests/RailwayInternalsTests.cs index b0f8bde..8156c2a 100644 --- a/Zooper.Bee.Tests/WorkflowInternalsTests.cs +++ b/Zooper.Bee.Tests/RailwayInternalsTests.cs @@ -6,9 +6,9 @@ namespace Zooper.Bee.Tests; /// -/// Tests for the internal execution logic of workflows using end-to-end tests. +/// Tests for the internal execution logic of railways using end-to-end tests. /// -public class WorkflowInternalsTests +public class RailwayInternalsTests { #region Test Models // Models for the tests @@ -23,7 +23,7 @@ private record TestError(string Code, string Message); public async Task DynamicBranchExecution_ConditionTrue_ExecutesActivities() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( // Create the payload from the request request => new TestPayload(request.Name, request.Value), @@ -69,7 +69,7 @@ public async Task DynamicBranchExecution_ConditionTrue_ExecutesActivities() public async Task DynamicBranchExecution_ConditionFalse_SkipsActivities() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "No result") ) @@ -111,7 +111,7 @@ public async Task DynamicBranchExecution_ConditionFalse_SkipsActivities() public async Task DynamicBranchExecution_ActivityReturnsError_PropagatesError() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "No result") ) @@ -147,7 +147,7 @@ public async Task DynamicBranchExecution_ActivityReturnsError_PropagatesError() public async Task DynamicBranchExecution_MultipleActivities_ExecutesInOrder() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "No result") ) @@ -208,7 +208,7 @@ public async Task DynamicBranchExecution_MultipleActivities_ExecutesInOrder() public async Task DynamicBranchExecution_MultipleBranches_ExecuteIndependently() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "No result") ) diff --git a/Zooper.Bee.Tests/WorkflowTests.cs b/Zooper.Bee.Tests/RailwayTests.cs similarity index 91% rename from Zooper.Bee.Tests/WorkflowTests.cs rename to Zooper.Bee.Tests/RailwayTests.cs index fc28439..f118fd7 100644 --- a/Zooper.Bee.Tests/WorkflowTests.cs +++ b/Zooper.Bee.Tests/RailwayTests.cs @@ -5,7 +5,7 @@ namespace Zooper.Bee.Tests; -public class WorkflowTests +public class RailwayTests { #region Test Models // Request model @@ -30,7 +30,7 @@ private record TestError(string Code, string Message); public async Task Execute_ValidRequest_ReturnsSuccessResult() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( // Create the payload from the request request => new TestPayload(request.Name, request.Value), @@ -69,7 +69,7 @@ public async Task Execute_ValidRequest_ReturnsSuccessResult() public async Task Execute_WithValidation_RejectsInvalidRequest() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "Default") ) @@ -99,7 +99,7 @@ public async Task Execute_WithValidation_RejectsInvalidRequest() public async Task Execute_WithConditionalActivity_OnlyExecutesWhenConditionMet() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "Default") ) @@ -137,7 +137,7 @@ public async Task Execute_WithConditionalActivity_OnlyExecutesWhenConditionMet() public async Task Execute_WithErrorInActivity_ReturnsError() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "Default") ) @@ -171,7 +171,7 @@ public async Task Execute_WithFinallyActivities_ExecutesThemEvenOnError() // Arrange bool finallyExecuted = false; - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new TestPayload(request.Name, request.Value), payload => new TestSuccess(payload.Result ?? "Default") ) diff --git a/Zooper.Bee.Tests/WorkflowWithContextTests.cs b/Zooper.Bee.Tests/RailwayWithContextTests.cs similarity index 95% rename from Zooper.Bee.Tests/WorkflowWithContextTests.cs rename to Zooper.Bee.Tests/RailwayWithContextTests.cs index 93cf514..a9c5855 100644 --- a/Zooper.Bee.Tests/WorkflowWithContextTests.cs +++ b/Zooper.Bee.Tests/RailwayWithContextTests.cs @@ -6,13 +6,13 @@ namespace Zooper.Bee.Tests; -public class WorkflowWithContextTests +public class RailwayWithContextTests { #region Test Models // Request model private record ProductRequest(int Id, string Name, decimal Price, bool NeedsCustomProcessing); - // Main workflow payload model + // Main railway payload model private record ProductPayload( int Id, string Name, @@ -45,7 +45,7 @@ private record ProductError(string Code, string Message); public async Task WithContext_ExecutesWhenConditionIsTrue() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( // Create the main payload from the request request => new ProductPayload( request.Id, @@ -146,7 +146,7 @@ public async Task WithContext_ExecutesWhenConditionIsTrue() public async Task WithContext_LocalPayloadIsolated_NotAffectedByOtherActivities() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, @@ -234,7 +234,7 @@ public async Task WithContext_LocalPayloadIsolated_NotAffectedByOtherActivities( public async Task WithContext_ErrorInContext_StopsExecutionAndReturnsError() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, @@ -286,7 +286,7 @@ public async Task WithContext_ErrorInContext_StopsExecutionAndReturnsError() public async Task WithContext_MultipleActivitiesInSameContext_ShareLocalPayload() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, @@ -367,7 +367,7 @@ public async Task WithContext_MultipleActivitiesInSameContext_ShareLocalPayload( public async Task WithContext_UnconditionalContext_AlwaysExecutes() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, @@ -413,7 +413,7 @@ public async Task WithContext_UnconditionalContext_AlwaysExecutes() public async Task WithContext_UnconditionalContextFluentApi_AlwaysExecutes() { // Arrange - var workflow = new WorkflowBuilder( + var workflow = new RailwayBuilder( request => new ProductPayload(request.Id, request.Name, request.Price, request.NeedsCustomProcessing), payload => new ProductResult( payload.Id, payload.Name, payload.FinalPrice, diff --git a/Zooper.Bee/BranchBuilder.cs b/Zooper.Bee/BranchBuilder.cs index 834aec8..0640e53 100644 --- a/Zooper.Bee/BranchBuilder.cs +++ b/Zooper.Bee/BranchBuilder.cs @@ -11,11 +11,11 @@ namespace Zooper.Bee; /// public sealed class BranchBuilder { - private readonly WorkflowBuilder _workflow; + private readonly RailwayBuilder _workflow; private readonly Branch _branch; internal BranchBuilder( - WorkflowBuilder workflow, + RailwayBuilder workflow, Branch branch) { _workflow = workflow; @@ -29,7 +29,7 @@ internal BranchBuilder( /// The branch builder for fluent chaining public BranchBuilder Do(Func>> activity) { - _branch.Activities.Add(new WorkflowStep(activity)); + _branch.Activities.Add(new RailwayStep(activity)); return this; } @@ -40,7 +40,7 @@ public BranchBuilder Do(FuncThe branch builder for fluent chaining public BranchBuilder Do(Func> activity) { - _branch.Activities.Add(new WorkflowStep((payload, _) => + _branch.Activities.Add(new RailwayStep((payload, _) => Task.FromResult(activity(payload)) )); return this; @@ -55,7 +55,7 @@ public BranchBuilder DoAll(params Func(activity)); + _branch.Activities.Add(new RailwayStep(activity)); } return this; } @@ -69,7 +69,7 @@ public BranchBuilder DoAll(params Func((payload, _) => + _branch.Activities.Add(new RailwayStep((payload, _) => Task.FromResult(activity(payload)) )); } @@ -81,7 +81,7 @@ public BranchBuilder DoAll(params Func /// The main workflow builder [Obsolete("EndBranch is deprecated. Use the new callback-style Branch method instead: .Branch(condition, branch => branch.Do(...))")] - public WorkflowBuilder EndBranch() + public RailwayBuilder EndBranch() { return _workflow; } diff --git a/Zooper.Bee/Extensions/RailwayExtensions.cs b/Zooper.Bee/Extensions/RailwayExtensions.cs new file mode 100644 index 0000000..6c97fc0 --- /dev/null +++ b/Zooper.Bee/Extensions/RailwayExtensions.cs @@ -0,0 +1,100 @@ +using System; +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; + +/// +/// Provides extension methods for registering all railway components (validations, steps, and railway classes) +/// with dependency injection. These methods simplify the configuration process by centralizing the registration +/// of all railway-related services. +/// +public static class RailwayExtensions +{ + /// + /// Executes a railway that doesn't require a request parameter. + /// + /// The type of the success result + /// The type of the error result + /// The railway to execute + /// The result of the railway execution + public static Task> Execute(this Railway railway) + { + return railway.Execute(Unit.Value); + } + + /// + /// Executes a railway that doesn't require a request parameter. + /// + /// The type of the success result + /// The type of the error result + /// The railway to execute + /// A cancellation token to observe while waiting for the task to complete + /// The result of the railway execution + public static Task> Execute( + this Railway railway, + CancellationToken cancellationToken) + { + return railway.Execute(Unit.Value, cancellationToken); + } + + /// + /// Registers all railway components from the specified assemblies into the service collection. + /// This includes railway validations, railway steps, and concrete railway classes. + /// + /// The service collection to add the registrations to + /// Optional list of assemblies to scan for railway components. If null or empty, all non-system + /// assemblies in the current AppDomain will be scanned + /// The service lifetime to use for the registered services (defaults to Scoped) + /// The service collection for chaining additional registrations + /// + /// This method provides a comprehensive registration of all railway-related components: + /// - Railway validations (via AddRailwayValidations) + /// - Railway steps (via AddRailwaySteps) + /// - Railway guards (via AddRailwayGuards) + /// - Concrete railway classes (classes ending with "Railway" or "Workflow") + /// + /// Railway classes are registered as themselves (not by interface) to support direct injection. + /// System and Microsoft assemblies are excluded by default when no specific assemblies are provided. + /// + public static IServiceCollection AddRailways( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + var assembliesToScan = assemblies != null + ? assemblies.ToArray() + : AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) + .ToArray(); + + // Register all railway validations + services.AddRailwayValidations(assembliesToScan, lifetime); + + // Register all railway guards + services.AddRailwayGuards(assembliesToScan, lifetime); + + // Register all railway steps + services.AddRailwaySteps(assembliesToScan, lifetime); + + // Then register all classes ending with Railway or Workflow (for backward compat) + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => + classes.Where(type => + (type.Name.EndsWith("Railway") || type.Name.EndsWith("Workflow")) + && type is { IsAbstract: false, IsInterface: false }) + ) + .AsSelf() + .WithLifetime(lifetime) + ); + + return services; + } +} diff --git a/Zooper.Bee/Extensions/RailwayGuardExtensions.cs b/Zooper.Bee/Extensions/RailwayGuardExtensions.cs new file mode 100644 index 0000000..66bdf2c --- /dev/null +++ b/Zooper.Bee/Extensions/RailwayGuardExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Zooper.Bee.Interfaces; + +namespace Zooper.Bee.Extensions; + +/// +/// Provides extension methods for registering railway guards with dependency injection. +/// +public static class RailwayGuardExtensions +{ + /// + /// Registers all railway guards from the specified assemblies into the service collection. + /// This includes both individual railway guards (IRailwayGuard) and guard collections (IRailwayGuards). + /// + /// The service collection to add the registrations to + /// Optional list of assemblies to scan for railway guards. If null or empty, all non-system + /// assemblies in the current AppDomain will be scanned + /// The service lifetime to use for the registered services (defaults to Scoped) + /// The service collection for chaining additional registrations + public static IServiceCollection AddRailwayGuards( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + var assembliesToScan = assemblies != null + ? assemblies.ToArray() + : AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) + .ToArray(); + + // Register all IRailwayGuard implementations (also finds IWorkflowGuard since it inherits IRailwayGuard) + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwayGuard))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IRailwayGuards implementations (also finds IWorkflowGuards since it inherits IRailwayGuards) + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwayGuards))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } +} diff --git a/Zooper.Bee/Extensions/RailwayStepsExtensions.cs b/Zooper.Bee/Extensions/RailwayStepsExtensions.cs new file mode 100644 index 0000000..cd0f8a1 --- /dev/null +++ b/Zooper.Bee/Extensions/RailwayStepsExtensions.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Zooper.Bee.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Zooper.Bee.Extensions; + +/// +/// Extension methods for registering railway steps with dependency injection +/// +public static class RailwayStepsExtensions +{ + /// + /// Adds all railway steps from the specified assemblies, or from all loaded assemblies if none specified + /// + /// The service collection + /// Optional list of assemblies to scan. If null or empty, scans all loaded assemblies + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddRailwaySteps( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + assemblies ??= AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")); + + // Register all IRailwayStep implementations (also finds IWorkflowStep since it inherits IRailwayStep) + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwayStep))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IRailwaySteps implementations (also finds IWorkflowSteps since it inherits IRailwaySteps) + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwaySteps))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } + + /// + /// Adds all railway steps from the assemblies containing the specified marker types + /// + /// The service collection + /// Types whose assemblies will be scanned + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddRailwayStepsFromAssembliesContaining( + this IServiceCollection services, + IEnumerable markerTypes, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + if (markerTypes == null) throw new ArgumentNullException(nameof(markerTypes)); + + var assemblies = markerTypes.Select(t => t.Assembly).Distinct(); + return services.AddRailwaySteps(assemblies, lifetime); + } + + /// + /// Adds all railway steps from the assemblies containing the specified marker types + /// + /// The service collection + /// The service lifetime (defaults to Scoped) + /// Types whose assemblies will be scanned + /// The service collection for chaining + public static IServiceCollection AddRailwayStepsFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime, + params Type[] markerTypes) + { + return services.AddRailwayStepsFromAssembliesContaining(markerTypes, lifetime); + } + + /// + /// Adds all railway steps from the assemblies containing the specified marker types + /// + /// The service collection + /// Types whose assemblies will be scanned + /// The service collection for chaining + public static IServiceCollection AddRailwayStepsFromAssembliesContaining( + this IServiceCollection services, + params Type[] markerTypes) + { + return services.AddRailwayStepsFromAssembliesContaining(markerTypes, ServiceLifetime.Scoped); + } + + /// + /// Adds all railway steps from the assemblies containing the specified marker types + /// + /// First marker type whose assembly will be scanned + /// The service collection + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddRailwayStepsFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddRailwayStepsFromAssembliesContaining( + [typeof(T1)], + lifetime + ); + } + + /// + /// Adds all railway steps from the assemblies containing the specified marker types + /// + /// First marker type whose assembly will be scanned + /// Second marker type whose assembly will be scanned + /// The service collection + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddRailwayStepsFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddRailwayStepsFromAssembliesContaining( + [typeof(T1), typeof(T2)], + lifetime + ); + } + + /// + /// Adds all railway steps from the assemblies containing the specified marker types + /// + /// First marker type whose assembly will be scanned + /// Second marker type whose assembly will be scanned + /// Third marker type whose assembly will be scanned + /// The service collection + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddRailwayStepsFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddRailwayStepsFromAssembliesContaining( + [typeof(T1), typeof(T2), typeof(T3)], + lifetime + ); + } + + /// + /// Adds all railway steps of specific types from the specified assemblies, or from all loaded assemblies if none specified + /// + /// The type of payload the steps process + /// The type of error the steps might return + /// The service collection + /// Optional list of assemblies to scan. If null or empty, scans all loaded assemblies + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddRailwaySteps( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + assemblies ??= AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")); + + // Register all IRailwayStep implementations + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwayStep))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IRailwaySteps implementations + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwaySteps))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } +} diff --git a/Zooper.Bee/Extensions/RailwayValidationExtensions.cs b/Zooper.Bee/Extensions/RailwayValidationExtensions.cs new file mode 100644 index 0000000..da7b22e --- /dev/null +++ b/Zooper.Bee/Extensions/RailwayValidationExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Zooper.Bee.Interfaces; + +namespace Zooper.Bee.Extensions; + +/// +/// Provides extension methods for registering railway validations with dependency injection. +/// +public static class RailwayValidationExtensions +{ + /// + /// Registers all railway validations from the specified assemblies into the service collection. + /// This includes both individual railway validations (IRailwayValidation) and validation collections (IRailwayValidations). + /// + /// The service collection to add the registrations to + /// Optional list of assemblies to scan for railway validations. If null or empty, all non-system + /// assemblies in the current AppDomain will be scanned + /// The service lifetime to use for the registered services (defaults to Scoped) + /// The service collection for chaining additional registrations + public static IServiceCollection AddRailwayValidations( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + var assembliesToScan = assemblies != null + ? assemblies.ToArray() + : AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) + .ToArray(); + + // Register all IRailwayValidation implementations (also finds IWorkflowValidation since it inherits IRailwayValidation) + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwayValidation))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IRailwayValidations implementations (also finds IWorkflowValidations since it inherits IRailwayValidations) + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo(typeof(IRailwayValidations))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } +} diff --git a/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs b/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs index 192edf7..788651b 100644 --- a/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs +++ b/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs @@ -10,98 +10,77 @@ namespace Zooper.Bee.Extensions; /// /// Extension methods for registering workflow activities with dependency injection /// -[Obsolete("Use WorkflowStepsExtensions instead. This class will be removed in a future version.")] +[Obsolete("Use RailwayStepsExtensions instead. This class will be removed in a future version.")] public static class WorkflowActivitiesExtensions { /// /// Adds all workflow activities from the specified assemblies, or from all loaded assemblies if none specified /// - [Obsolete("Use AddWorkflowSteps instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwaySteps instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivities( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowSteps(assemblies, lifetime); + return services.AddRailwaySteps(assemblies, lifetime); } - /// - /// Adds all workflow activities from the assemblies containing the specified marker types - /// - [Obsolete("Use AddWorkflowStepsFromAssembliesContaining instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( this IServiceCollection services, IEnumerable markerTypes, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining(markerTypes, lifetime); + return services.AddRailwayStepsFromAssembliesContaining(markerTypes, lifetime); } - /// - /// Adds all workflow activities from the assemblies containing the specified marker types - /// - [Obsolete("Use AddWorkflowStepsFromAssembliesContaining instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime, params Type[] markerTypes) { - return services.AddWorkflowStepsFromAssembliesContaining(lifetime, markerTypes); + return services.AddRailwayStepsFromAssembliesContaining(lifetime, markerTypes); } - /// - /// Adds all workflow activities from the assemblies containing the specified marker types - /// - [Obsolete("Use AddWorkflowStepsFromAssembliesContaining instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( this IServiceCollection services, params Type[] markerTypes) { - return services.AddWorkflowStepsFromAssembliesContaining(markerTypes); + return services.AddRailwayStepsFromAssembliesContaining(markerTypes); } - /// - /// Adds all workflow activities from the assemblies containing the specified marker types - /// - [Obsolete("Use AddWorkflowStepsFromAssembliesContaining instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining(lifetime); + return services.AddRailwayStepsFromAssembliesContaining(lifetime); } - /// - /// Adds all workflow activities from the assemblies containing the specified marker types - /// - [Obsolete("Use AddWorkflowStepsFromAssembliesContaining instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining(lifetime); + return services.AddRailwayStepsFromAssembliesContaining(lifetime); } - /// - /// Adds all workflow activities from the assemblies containing the specified marker types - /// - [Obsolete("Use AddWorkflowStepsFromAssembliesContaining instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining(lifetime); + return services.AddRailwayStepsFromAssembliesContaining(lifetime); } - /// - /// Adds all workflow activities of specific types from the specified assemblies, or from all loaded assemblies if none specified - /// - [Obsolete("Use AddWorkflowSteps instead. This method will be removed in a future version.")] + [Obsolete("Use AddRailwaySteps instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowActivities( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowSteps(assemblies, lifetime); + return services.AddRailwaySteps(assemblies, lifetime); } } diff --git a/Zooper.Bee/Extensions/WorkflowExtensions.cs b/Zooper.Bee/Extensions/WorkflowExtensions.cs index f45527a..c16c6e6 100644 --- a/Zooper.Bee/Extensions/WorkflowExtensions.cs +++ b/Zooper.Bee/Extensions/WorkflowExtensions.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -10,19 +9,15 @@ namespace Zooper.Bee.Extensions; /// -/// Provides extension methods for registering all workflow components (validations, activities, and workflow classes) -/// with dependency injection. These methods simplify the configuration process by centralizing the registration -/// of all workflow-related services. +/// Provides extension methods for registering all workflow components with dependency injection. /// +[Obsolete("Use RailwayExtensions instead. This class will be removed in a future version.")] 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 + [Obsolete("Use the Railway Execute extension method instead. This method will be removed in a future version.")] public static Task> Execute(this Workflow workflow) { return workflow.Execute(Unit.Value); @@ -31,11 +26,7 @@ public static Task> Execute(this Work /// /// 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 + [Obsolete("Use the Railway Execute extension method instead. This method will be removed in a future version.")] public static Task> Execute( this Workflow workflow, CancellationToken cancellationToken) @@ -45,53 +36,13 @@ public static Task> Execute( /// /// Registers all workflow components from the specified assemblies into the service collection. - /// This includes workflow validations, workflow activities, and concrete workflow classes. /// - /// The service collection to add the registrations to - /// Optional list of assemblies to scan for workflow components. If null or empty, all non-system - /// assemblies in the current AppDomain will be scanned - /// The service lifetime to use for the registered services (defaults to Scoped) - /// The service collection for chaining additional registrations - /// - /// This method provides a comprehensive registration of all workflow-related components: - /// - Workflow validations (via AddWorkflowValidations) - /// - Workflow activities (via AddWorkflowActivities) - /// - Concrete workflow classes (classes ending with "Workflow") - /// - /// Workflow classes are registered as themselves (not by interface) to support direct injection. - /// System and Microsoft assemblies are excluded by default when no specific assemblies are provided. - /// + [Obsolete("Use AddRailways instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflows( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - // If no assemblies are specified, use all loaded assemblies - var assembliesToScan = assemblies != null - ? assemblies.ToArray() - : AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) - .ToArray(); - - // Register all workflow validations - services.AddWorkflowValidations(assembliesToScan, lifetime); - - // Register all workflow guards - services.AddWorkflowGuards(assembliesToScan, lifetime); - - // Register all workflow steps - services.AddWorkflowSteps(assembliesToScan, lifetime); - - // Then register all classes ending with Workflow - services.Scan(scan => scan - .FromAssemblies(assembliesToScan) - .AddClasses(classes => - classes.Where(type => type.Name.EndsWith("Workflow") && type is { IsAbstract: false, IsInterface: false }) - ) - .AsSelf() - .WithLifetime(lifetime) - ); - - return services; + return services.AddRailways(assemblies, lifetime); } -} \ No newline at end of file +} diff --git a/Zooper.Bee/Extensions/WorkflowGuardExtensions.cs b/Zooper.Bee/Extensions/WorkflowGuardExtensions.cs index 0142a2a..1f11128 100644 --- a/Zooper.Bee/Extensions/WorkflowGuardExtensions.cs +++ b/Zooper.Bee/Extensions/WorkflowGuardExtensions.cs @@ -1,54 +1,25 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; -using Zooper.Bee.Interfaces; namespace Zooper.Bee.Extensions; /// /// Provides extension methods for registering workflow guards with dependency injection. /// +[Obsolete("Use RailwayGuardExtensions instead. This class will be removed in a future version.")] public static class WorkflowGuardExtensions { /// /// Registers all workflow guards from the specified assemblies into the service collection. - /// This includes both individual workflow guards (IWorkflowGuard) and guard collections (IWorkflowGuards). /// - /// The service collection to add the registrations to - /// Optional list of assemblies to scan for workflow guards. If null or empty, all non-system - /// assemblies in the current AppDomain will be scanned - /// The service lifetime to use for the registered services (defaults to Scoped) - /// The service collection for chaining additional registrations + [Obsolete("Use AddRailwayGuards instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowGuards( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - // If no assemblies are specified, use all loaded assemblies - var assembliesToScan = assemblies != null - ? assemblies.ToArray() - : AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) - .ToArray(); - - // Register all IWorkflowGuard implementations - services.Scan(scan => scan - .FromAssemblies(assembliesToScan) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowGuard))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - // Register all IWorkflowGuards implementations - services.Scan(scan => scan - .FromAssemblies(assembliesToScan) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowGuards))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - return services; + return services.AddRailwayGuards(assemblies, lifetime); } -} \ No newline at end of file +} diff --git a/Zooper.Bee/Extensions/WorkflowStepsExtensions.cs b/Zooper.Bee/Extensions/WorkflowStepsExtensions.cs index 40bf666..a6cd800 100644 --- a/Zooper.Bee/Extensions/WorkflowStepsExtensions.cs +++ b/Zooper.Bee/Extensions/WorkflowStepsExtensions.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; -using Zooper.Bee.Interfaces; // ReSharper disable MemberCanBePrivate.Global @@ -12,183 +10,98 @@ namespace Zooper.Bee.Extensions; /// /// Extension methods for registering workflow steps with dependency injection /// +[Obsolete("Use RailwayStepsExtensions instead. This class will be removed in a future version.")] public static class WorkflowStepsExtensions { /// /// Adds all workflow steps from the specified assemblies, or from all loaded assemblies if none specified /// - /// The service collection - /// Optional list of assemblies to scan. If null or empty, scans all loaded assemblies - /// The service lifetime (defaults to Scoped) - /// The service collection for chaining + [Obsolete("Use AddRailwaySteps instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowSteps( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - // If no assemblies are specified, use all loaded assemblies - assemblies ??= AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")); - - // Register all IWorkflowStep implementations - services.Scan(scan => scan - .FromAssemblies(assemblies) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowStep))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - // Register all IWorkflowSteps implementations - services.Scan(scan => scan - .FromAssemblies(assemblies) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowSteps))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - return services; + return services.AddRailwaySteps(assemblies, lifetime); } /// /// Adds all workflow steps from the assemblies containing the specified marker types /// - /// The service collection - /// Types whose assemblies will be scanned - /// The service lifetime (defaults to Scoped) - /// The service collection for chaining + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowStepsFromAssembliesContaining( this IServiceCollection services, IEnumerable markerTypes, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - if (markerTypes == null) throw new ArgumentNullException(nameof(markerTypes)); - - var assemblies = markerTypes.Select(t => t.Assembly).Distinct(); - return services.AddWorkflowSteps(assemblies, lifetime); + return services.AddRailwayStepsFromAssembliesContaining(markerTypes, lifetime); } /// /// Adds all workflow steps from the assemblies containing the specified marker types /// - /// The service collection - /// The service lifetime (defaults to Scoped) - /// Types whose assemblies will be scanned - /// The service collection for chaining + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowStepsFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime, params Type[] markerTypes) { - return services.AddWorkflowStepsFromAssembliesContaining(markerTypes, lifetime); + return services.AddRailwayStepsFromAssembliesContaining(lifetime, markerTypes); } /// /// Adds all workflow steps from the assemblies containing the specified marker types /// - /// The service collection - /// Types whose assemblies will be scanned - /// The service collection for chaining + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowStepsFromAssembliesContaining( this IServiceCollection services, params Type[] markerTypes) { - return services.AddWorkflowStepsFromAssembliesContaining(markerTypes, ServiceLifetime.Scoped); + return services.AddRailwayStepsFromAssembliesContaining(markerTypes); } /// /// Adds all workflow steps from the assemblies containing the specified marker types /// - /// First marker type whose assembly will be scanned - /// The service collection - /// The service lifetime (defaults to Scoped) - /// The service collection for chaining + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowStepsFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining( - [ - typeof(T1) - ], - lifetime - ); + return services.AddRailwayStepsFromAssembliesContaining(lifetime); } /// /// Adds all workflow steps from the assemblies containing the specified marker types /// - /// First marker type whose assembly will be scanned - /// Second marker type whose assembly will be scanned - /// The service collection - /// The service lifetime (defaults to Scoped) - /// The service collection for chaining + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowStepsFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining( - [ - typeof(T1), typeof(T2) - ], - lifetime - ); + return services.AddRailwayStepsFromAssembliesContaining(lifetime); } /// /// Adds all workflow steps from the assemblies containing the specified marker types /// - /// First marker type whose assembly will be scanned - /// Second marker type whose assembly will be scanned - /// Third marker type whose assembly will be scanned - /// The service collection - /// The service lifetime (defaults to Scoped) - /// The service collection for chaining + [Obsolete("Use AddRailwayStepsFromAssembliesContaining instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowStepsFromAssembliesContaining( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - return services.AddWorkflowStepsFromAssembliesContaining( - [ - typeof(T1), typeof(T2), typeof(T3) - ], - lifetime - ); + return services.AddRailwayStepsFromAssembliesContaining(lifetime); } /// - /// Adds all workflow steps of specific types from the specified assemblies, or from all loaded assemblies if none specified + /// Adds all workflow steps of specific types from the specified assemblies /// - /// The type of payload the steps process - /// The type of error the steps might return - /// The service collection - /// Optional list of assemblies to scan. If null or empty, scans all loaded assemblies - /// The service lifetime (defaults to Scoped) - /// The service collection for chaining + [Obsolete("Use AddRailwaySteps instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowSteps( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - // If no assemblies are specified, use all loaded assemblies - assemblies ??= AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")); - - // Register all IWorkflowStep implementations - services.Scan(scan => scan - .FromAssemblies(assemblies) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowStep))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - // Register all IWorkflowSteps implementations - services.Scan(scan => scan - .FromAssemblies(assemblies) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowSteps))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - return services; + return services.AddRailwaySteps(assemblies, lifetime); } } diff --git a/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs b/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs index d5a57a2..0e81aaa 100644 --- a/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs +++ b/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs @@ -1,60 +1,25 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; -using Zooper.Bee.Interfaces; namespace Zooper.Bee.Extensions; /// /// Provides extension methods for registering workflow validations with dependency injection. -/// These methods use assembly scanning to automatically discover and register all implementations -/// of workflow validation interfaces within specified assemblies. /// +[Obsolete("Use RailwayValidationExtensions instead. This class will be removed in a future version.")] public static class WorkflowValidationExtensions { /// /// Registers all workflow validations from the specified assemblies into the service collection. - /// This includes both individual workflow validations (IWorkflowValidation) and validation collections (IWorkflowValidations). /// - /// The service collection to add the registrations to - /// Optional list of assemblies to scan for workflow validations. If null or empty, all non-system - /// assemblies in the current AppDomain will be scanned - /// The service lifetime to use for the registered services (defaults to Scoped) - /// The service collection for chaining additional registrations - /// - /// This method uses Scrutor to scan assemblies and register classes based on their implemented interfaces. - /// System and Microsoft assemblies are excluded by default when no specific assemblies are provided. - /// + [Obsolete("Use AddRailwayValidations instead. This method will be removed in a future version.")] public static IServiceCollection AddWorkflowValidations( this IServiceCollection services, IEnumerable? assemblies = null, ServiceLifetime lifetime = ServiceLifetime.Scoped) { - // If no assemblies are specified, use all loaded assemblies - var assembliesToScan = assemblies != null - ? assemblies.ToArray() - : AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) - .ToArray(); - - // Register all IWorkflowValidation implementations - services.Scan(scan => scan - .FromAssemblies(assembliesToScan) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowValidation))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - // Register all IWorkflowValidations implementations - services.Scan(scan => scan - .FromAssemblies(assembliesToScan) - .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowValidations))) - .AsImplementedInterfaces() - .WithLifetime(lifetime) - ); - - return services; + return services.AddRailwayValidations(assemblies, lifetime); } -} \ No newline at end of file +} diff --git a/Zooper.Bee/Features/Context/Context.cs b/Zooper.Bee/Features/Context/Context.cs index c5442fa..7e6aff3 100644 --- a/Zooper.Bee/Features/Context/Context.cs +++ b/Zooper.Bee/Features/Context/Context.cs @@ -9,7 +9,7 @@ namespace Zooper.Bee.Features.Context; /// Type of the main workflow payload /// Type of the local context state /// Type of the error -internal sealed class Context : IWorkflowFeature +internal sealed class Context : IRailwayFeature { /// /// The condition that determines if this context should execute. diff --git a/Zooper.Bee/Features/Context/ContextBuilder.cs b/Zooper.Bee/Features/Context/ContextBuilder.cs index fea807a..16fde96 100644 --- a/Zooper.Bee/Features/Context/ContextBuilder.cs +++ b/Zooper.Bee/Features/Context/ContextBuilder.cs @@ -15,11 +15,11 @@ namespace Zooper.Bee.Features.Context; /// The type of the error result public sealed class ContextBuilder { - private readonly WorkflowBuilder _workflow; + private readonly RailwayBuilder _workflow; private readonly Context _context; internal ContextBuilder( - WorkflowBuilder workflow, + RailwayBuilder workflow, Context context) { _workflow = workflow; diff --git a/Zooper.Bee/Features/Detached/Detached.cs b/Zooper.Bee/Features/Detached/Detached.cs index c4549ae..77099ca 100644 --- a/Zooper.Bee/Features/Detached/Detached.cs +++ b/Zooper.Bee/Features/Detached/Detached.cs @@ -9,7 +9,7 @@ namespace Zooper.Bee.Features.Detached; /// /// Type of the main workflow payload /// Type of the error -internal sealed class Detached : IWorkflowFeature +internal sealed class Detached : IRailwayFeature { /// /// The condition that determines if this detached group should execute. @@ -24,7 +24,7 @@ internal sealed class Detached : IWorkflowFeature /// The list of activities in this detached group. /// - public List> Activities { get; } = new(); + public List> Activities { get; } = new(); /// /// Creates a new detached group with an optional condition. diff --git a/Zooper.Bee/Features/Detached/DetachedBuilder.cs b/Zooper.Bee/Features/Detached/DetachedBuilder.cs index aaf5602..fcd0eed 100644 --- a/Zooper.Bee/Features/Detached/DetachedBuilder.cs +++ b/Zooper.Bee/Features/Detached/DetachedBuilder.cs @@ -15,11 +15,11 @@ namespace Zooper.Bee.Features.Detached; /// The type of the error result public sealed class DetachedBuilder { - private readonly WorkflowBuilder _workflow; + private readonly RailwayBuilder _workflow; private readonly Detached _detached; internal DetachedBuilder( - WorkflowBuilder workflow, + RailwayBuilder workflow, Detached detached) { _workflow = workflow; @@ -34,7 +34,7 @@ internal DetachedBuilder( public DetachedBuilder Do( Func>> activity) { - _detached.Activities.Add(new WorkflowStep(activity)); + _detached.Activities.Add(new RailwayStep(activity)); return this; } @@ -46,7 +46,7 @@ public DetachedBuilder Do( public DetachedBuilder Do( Func> activity) { - _detached.Activities.Add(new WorkflowStep( + _detached.Activities.Add(new RailwayStep( (payload, _) => Task.FromResult(activity(payload)) )); return this; @@ -62,7 +62,7 @@ public DetachedBuilder DoAll( { foreach (var activity in activities) { - _detached.Activities.Add(new WorkflowStep(activity)); + _detached.Activities.Add(new RailwayStep(activity)); } return this; } @@ -77,7 +77,7 @@ public DetachedBuilder DoAll( { foreach (var activity in activities) { - _detached.Activities.Add(new WorkflowStep( + _detached.Activities.Add(new RailwayStep( (payload, _) => Task.FromResult(activity(payload)) )); } diff --git a/Zooper.Bee/Features/Group/Group.cs b/Zooper.Bee/Features/Group/Group.cs index 3c40c9d..35ea2bd 100644 --- a/Zooper.Bee/Features/Group/Group.cs +++ b/Zooper.Bee/Features/Group/Group.cs @@ -9,7 +9,7 @@ namespace Zooper.Bee.Features.Group; /// /// Type of the main workflow payload /// Type of the error -internal sealed class Group : IWorkflowFeature +internal sealed class Group : IRailwayFeature { /// /// The condition that determines if this group should execute. @@ -24,7 +24,7 @@ internal sealed class Group : IWorkflowFeature /// The list of activities in this group. /// - public List> Activities { get; } = new(); + public List> Activities { get; } = new(); /// /// Creates a new group with an optional condition. diff --git a/Zooper.Bee/Features/Group/GroupBuilder.cs b/Zooper.Bee/Features/Group/GroupBuilder.cs index 6dc9f61..af08df3 100644 --- a/Zooper.Bee/Features/Group/GroupBuilder.cs +++ b/Zooper.Bee/Features/Group/GroupBuilder.cs @@ -15,11 +15,11 @@ namespace Zooper.Bee.Features.Group; /// The type of the error result public sealed class GroupBuilder { - private readonly WorkflowBuilder _workflow; + private readonly RailwayBuilder _workflow; private readonly Group _group; internal GroupBuilder( - WorkflowBuilder workflow, + RailwayBuilder workflow, Group group) { _workflow = workflow; @@ -34,7 +34,7 @@ internal GroupBuilder( public GroupBuilder Do( Func>> activity) { - _group.Activities.Add(new WorkflowStep(activity)); + _group.Activities.Add(new RailwayStep(activity)); return this; } @@ -46,7 +46,7 @@ public GroupBuilder Do( public GroupBuilder Do( Func> activity) { - _group.Activities.Add(new WorkflowStep( + _group.Activities.Add(new RailwayStep( (payload, _) => Task.FromResult(activity(payload)) )); return this; @@ -62,7 +62,7 @@ public GroupBuilder DoAll( { foreach (var activity in activities) { - _group.Activities.Add(new WorkflowStep(activity)); + _group.Activities.Add(new RailwayStep(activity)); } return this; } @@ -77,7 +77,7 @@ public GroupBuilder DoAll( { foreach (var activity in activities) { - _group.Activities.Add(new WorkflowStep( + _group.Activities.Add(new RailwayStep( (payload, _) => Task.FromResult(activity(payload)) )); } diff --git a/Zooper.Bee/Features/IRailwayFeature.cs b/Zooper.Bee/Features/IRailwayFeature.cs new file mode 100644 index 0000000..366a90a --- /dev/null +++ b/Zooper.Bee/Features/IRailwayFeature.cs @@ -0,0 +1,21 @@ +using System; + +namespace Zooper.Bee.Features; + +/// +/// Base interface for all railway features. +/// +/// The type of the main railway payload +/// The type of the error +public interface IRailwayFeature +{ + /// + /// Gets the condition that determines if this feature should execute. + /// + Func? Condition { get; } + + /// + /// Whether this feature should merge back into the main railway. + /// + bool ShouldMerge { get; } +} diff --git a/Zooper.Bee/Features/IWorkflowFeature.cs b/Zooper.Bee/Features/IWorkflowFeature.cs index 8e0a657..6166e4d 100644 --- a/Zooper.Bee/Features/IWorkflowFeature.cs +++ b/Zooper.Bee/Features/IWorkflowFeature.cs @@ -7,15 +7,5 @@ namespace Zooper.Bee.Features; /// /// The type of the main workflow payload /// The type of the error -public interface IWorkflowFeature -{ - /// - /// Gets the condition that determines if this feature should execute. - /// - Func? Condition { get; } - - /// - /// Whether this feature should merge back into the main workflow. - /// - bool ShouldMerge { get; } -} \ No newline at end of file +[Obsolete("Use IRailwayFeature instead. This interface will be removed in a future version.")] +public interface IWorkflowFeature : IRailwayFeature; diff --git a/Zooper.Bee/Features/Parallel/Parallel.cs b/Zooper.Bee/Features/Parallel/Parallel.cs index 10bafe6..02248cc 100644 --- a/Zooper.Bee/Features/Parallel/Parallel.cs +++ b/Zooper.Bee/Features/Parallel/Parallel.cs @@ -9,7 +9,7 @@ namespace Zooper.Bee.Features.Parallel; /// /// Type of the main workflow payload /// Type of the error -internal sealed class Parallel : IWorkflowFeature +internal sealed class Parallel : IRailwayFeature { /// /// Parallel execution can have a condition, but typically runs unconditionally. diff --git a/Zooper.Bee/Features/Parallel/ParallelBuilder.cs b/Zooper.Bee/Features/Parallel/ParallelBuilder.cs index df1b33c..81f5786 100644 --- a/Zooper.Bee/Features/Parallel/ParallelBuilder.cs +++ b/Zooper.Bee/Features/Parallel/ParallelBuilder.cs @@ -12,11 +12,11 @@ namespace Zooper.Bee.Features.Parallel; /// The type of the error result public sealed class ParallelBuilder { - private readonly WorkflowBuilder _workflow; + private readonly RailwayBuilder _workflow; private readonly Parallel _parallel; internal ParallelBuilder( - WorkflowBuilder workflow, + RailwayBuilder workflow, Parallel parallel) { _workflow = workflow; diff --git a/Zooper.Bee/Features/Parallel/ParallelDetached.cs b/Zooper.Bee/Features/Parallel/ParallelDetached.cs index 8c4bba4..45abb22 100644 --- a/Zooper.Bee/Features/Parallel/ParallelDetached.cs +++ b/Zooper.Bee/Features/Parallel/ParallelDetached.cs @@ -9,7 +9,7 @@ namespace Zooper.Bee.Features.Parallel; /// /// Type of the main workflow payload /// Type of the error -internal sealed class ParallelDetached : IWorkflowFeature +internal sealed class ParallelDetached : IRailwayFeature { /// /// Parallel detached execution can have a condition, but typically runs unconditionally. diff --git a/Zooper.Bee/Features/Parallel/ParallelDetachedBuilder.cs b/Zooper.Bee/Features/Parallel/ParallelDetachedBuilder.cs index e30487c..ec97c03 100644 --- a/Zooper.Bee/Features/Parallel/ParallelDetachedBuilder.cs +++ b/Zooper.Bee/Features/Parallel/ParallelDetachedBuilder.cs @@ -12,11 +12,11 @@ namespace Zooper.Bee.Features.Parallel; /// The type of the error result public sealed class ParallelDetachedBuilder { - private readonly WorkflowBuilder _workflow; + private readonly RailwayBuilder _workflow; private readonly ParallelDetached _parallelDetached; internal ParallelDetachedBuilder( - WorkflowBuilder workflow, + RailwayBuilder workflow, ParallelDetached parallelDetached) { _workflow = workflow; diff --git a/Zooper.Bee/Interfaces/IRailwayGuard.cs b/Zooper.Bee/Interfaces/IRailwayGuard.cs new file mode 100644 index 0000000..848b406 --- /dev/null +++ b/Zooper.Bee/Interfaces/IRailwayGuard.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all railway guards. +/// +public interface IRailwayGuard; + +/// +/// Represents a guard that checks if a railway can be executed. +/// +/// The type of the request. +/// The type of the error. +public interface IRailwayGuard : IRailwayGuard +{ + /// + /// Checks if the railway can be executed with the given request. + /// + /// The railway request. + /// Token to observe for cancellation. + /// Either an error if the guard fails, or Unit if it succeeds. + Task> ExecuteAsync( + TRequest request, + CancellationToken cancellationToken); +} diff --git a/Zooper.Bee/Interfaces/IRailwayGuards.cs b/Zooper.Bee/Interfaces/IRailwayGuards.cs new file mode 100644 index 0000000..dfe275a --- /dev/null +++ b/Zooper.Bee/Interfaces/IRailwayGuards.cs @@ -0,0 +1,13 @@ +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all railway guards. +/// +public interface IRailwayGuards; + +/// +/// Interface for a collection of railway guards operating on the same request and error types. +/// +/// The type of the request. +/// The type of the error. +public interface IRailwayGuards : IRailwayGuards; diff --git a/Zooper.Bee/Interfaces/IRailwayStep.cs b/Zooper.Bee/Interfaces/IRailwayStep.cs new file mode 100644 index 0000000..8f726e9 --- /dev/null +++ b/Zooper.Bee/Interfaces/IRailwayStep.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all railway steps +/// +public interface IRailwayStep; + +/// +/// Interface for a step that works with a specific payload and error type +/// +/// The type of payload the step processes +/// The type of error the step might return +public interface IRailwayStep : IRailwayStep +{ + /// + /// Executes the step with the given payload + /// + /// The input payload + /// Cancellation token + /// Either the error or the updated payload + Task> Execute(TPayload payload, CancellationToken cancellationToken); +} diff --git a/Zooper.Bee/Interfaces/IRailwaySteps.cs b/Zooper.Bee/Interfaces/IRailwaySteps.cs new file mode 100644 index 0000000..6b938ca --- /dev/null +++ b/Zooper.Bee/Interfaces/IRailwaySteps.cs @@ -0,0 +1,13 @@ +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for railway step collections +/// +public interface IRailwaySteps; + +/// +/// Interface for a collection of railway steps operating on the same payload and error types +/// +/// The type of payload the steps process +/// The type of error the steps might return +public interface IRailwaySteps : IRailwaySteps; diff --git a/Zooper.Bee/Interfaces/IRailwayValidation.cs b/Zooper.Bee/Interfaces/IRailwayValidation.cs new file mode 100644 index 0000000..58f9bd0 --- /dev/null +++ b/Zooper.Bee/Interfaces/IRailwayValidation.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all railway validations +/// +public interface IRailwayValidation; + +/// +/// Interface for a validation that validates a request and potentially returns an error +/// +/// The type of request being validated +/// The type of error that might be returned +public interface IRailwayValidation : IRailwayValidation +{ + /// + /// Validates the request + /// + /// The request to validate + /// Cancellation token + /// An option containing an error if validation fails, or None if validation succeeds + Task> Validate( + TRequest request, + CancellationToken cancellationToken); +} diff --git a/Zooper.Bee/Interfaces/IRailwayValidations.cs b/Zooper.Bee/Interfaces/IRailwayValidations.cs new file mode 100644 index 0000000..feaf0c7 --- /dev/null +++ b/Zooper.Bee/Interfaces/IRailwayValidations.cs @@ -0,0 +1,13 @@ +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for a collection of railway validations +/// +public interface IRailwayValidations; + +/// +/// Interface for a collection of railway validations for a specific request and error type +/// +/// The type of request being validated +/// The type of error that might be returned +public interface IRailwayValidations : IRailwayValidations; diff --git a/Zooper.Bee/Interfaces/IWorkflowActivities.cs b/Zooper.Bee/Interfaces/IWorkflowActivities.cs index 7c40104..88e2427 100644 --- a/Zooper.Bee/Interfaces/IWorkflowActivities.cs +++ b/Zooper.Bee/Interfaces/IWorkflowActivities.cs @@ -5,13 +5,13 @@ namespace Zooper.Bee.Interfaces; /// /// Base marker interface for workflow activity collections /// -[Obsolete("Use IWorkflowSteps instead. This interface will be removed in a future version.")] -public interface IWorkflowActivities : IWorkflowSteps; +[Obsolete("Use IRailwaySteps instead. This interface will be removed in a future version.")] +public interface IWorkflowActivities : IRailwaySteps; /// /// Interface for a collection of workflow activities operating on the same payload and error types /// /// The type of payload the activities process /// The type of error the activities might return -[Obsolete("Use IWorkflowSteps instead. This interface will be removed in a future version.")] -public interface IWorkflowActivities : IWorkflowSteps; +[Obsolete("Use IRailwaySteps instead. This interface will be removed in a future version.")] +public interface IWorkflowActivities : IRailwaySteps; diff --git a/Zooper.Bee/Interfaces/IWorkflowActivity.cs b/Zooper.Bee/Interfaces/IWorkflowActivity.cs index 54c46bd..c58b594 100644 --- a/Zooper.Bee/Interfaces/IWorkflowActivity.cs +++ b/Zooper.Bee/Interfaces/IWorkflowActivity.cs @@ -5,13 +5,13 @@ namespace Zooper.Bee.Interfaces; /// /// Base marker interface for all workflow activities /// -[Obsolete("Use IWorkflowStep instead. This interface will be removed in a future version.")] -public interface IWorkflowActivity : IWorkflowStep; +[Obsolete("Use IRailwayStep instead. This interface will be removed in a future version.")] +public interface IWorkflowActivity : IRailwayStep; /// /// Interface for an activity that works with a specific payload and error type /// /// The type of payload the activity processes /// The type of error the activity might return -[Obsolete("Use IWorkflowStep instead. This interface will be removed in a future version.")] -public interface IWorkflowActivity : IWorkflowStep; +[Obsolete("Use IRailwayStep instead. This interface will be removed in a future version.")] +public interface IWorkflowActivity : IRailwayStep; diff --git a/Zooper.Bee/Interfaces/IWorkflowGuard.cs b/Zooper.Bee/Interfaces/IWorkflowGuard.cs index 18340e1..54c1a7c 100644 --- a/Zooper.Bee/Interfaces/IWorkflowGuard.cs +++ b/Zooper.Bee/Interfaces/IWorkflowGuard.cs @@ -1,28 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; -using Zooper.Fox; +using System; namespace Zooper.Bee.Interfaces; /// /// Base marker interface for all workflow guards. /// -public interface IWorkflowGuard; +[Obsolete("Use IRailwayGuard instead. This interface will be removed in a future version.")] +public interface IWorkflowGuard : IRailwayGuard; /// /// Represents a guard that checks if a workflow can be executed. /// /// The type of the request. /// The type of the error. -public interface IWorkflowGuard : IWorkflowGuard -{ - /// - /// Checks if the workflow can be executed with the given request. - /// - /// The workflow request. - /// Token to observe for cancellation. - /// Either an error if the guard fails, or Unit if it succeeds. - Task> ExecuteAsync( - TRequest request, - CancellationToken cancellationToken); -} \ No newline at end of file +[Obsolete("Use IRailwayGuard instead. This interface will be removed in a future version.")] +public interface IWorkflowGuard : IRailwayGuard; diff --git a/Zooper.Bee/Interfaces/IWorkflowGuards.cs b/Zooper.Bee/Interfaces/IWorkflowGuards.cs index 372a489..71a2c64 100644 --- a/Zooper.Bee/Interfaces/IWorkflowGuards.cs +++ b/Zooper.Bee/Interfaces/IWorkflowGuards.cs @@ -1,13 +1,17 @@ -namespace Zooper.Bee.Interfaces; +using System; + +namespace Zooper.Bee.Interfaces; /// /// Base marker interface for all workflow guards. /// -public interface IWorkflowGuards; +[Obsolete("Use IRailwayGuards instead. This interface will be removed in a future version.")] +public interface IWorkflowGuards : IRailwayGuards; /// /// Interface for a collection of workflow guards operating on the same request and error types. /// /// The type of the request. /// The type of the error. -public interface IWorkflowGuards : IWorkflowGuards; \ No newline at end of file +[Obsolete("Use IRailwayGuards instead. This interface will be removed in a future version.")] +public interface IWorkflowGuards : IRailwayGuards; diff --git a/Zooper.Bee/Interfaces/IWorkflowStep.cs b/Zooper.Bee/Interfaces/IWorkflowStep.cs index bb556e4..0dcde02 100644 --- a/Zooper.Bee/Interfaces/IWorkflowStep.cs +++ b/Zooper.Bee/Interfaces/IWorkflowStep.cs @@ -1,26 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; -using Zooper.Fox; +using System; namespace Zooper.Bee.Interfaces; /// /// Base marker interface for all workflow steps /// -public interface IWorkflowStep; +[Obsolete("Use IRailwayStep instead. This interface will be removed in a future version.")] +public interface IWorkflowStep : IRailwayStep; /// /// Interface for a step that works with a specific payload and error type /// /// The type of payload the step processes /// The type of error the step might return -public interface IWorkflowStep : IWorkflowStep -{ - /// - /// Executes the step with the given payload - /// - /// The input payload - /// Cancellation token - /// Either the error or the updated payload - Task> Execute(TPayload payload, CancellationToken cancellationToken); -} +[Obsolete("Use IRailwayStep instead. This interface will be removed in a future version.")] +public interface IWorkflowStep : IRailwayStep; diff --git a/Zooper.Bee/Interfaces/IWorkflowSteps.cs b/Zooper.Bee/Interfaces/IWorkflowSteps.cs index 9ad04ec..fe36629 100644 --- a/Zooper.Bee/Interfaces/IWorkflowSteps.cs +++ b/Zooper.Bee/Interfaces/IWorkflowSteps.cs @@ -1,13 +1,17 @@ +using System; + namespace Zooper.Bee.Interfaces; /// /// Base marker interface for workflow step collections /// -public interface IWorkflowSteps; +[Obsolete("Use IRailwaySteps instead. This interface will be removed in a future version.")] +public interface IWorkflowSteps : IRailwaySteps; /// /// Interface for a collection of workflow steps operating on the same payload and error types /// /// The type of payload the steps process /// The type of error the steps might return -public interface IWorkflowSteps : IWorkflowSteps; +[Obsolete("Use IRailwaySteps instead. This interface will be removed in a future version.")] +public interface IWorkflowSteps : IRailwaySteps; diff --git a/Zooper.Bee/Interfaces/IWorkflowValidation.cs b/Zooper.Bee/Interfaces/IWorkflowValidation.cs index 502c765..4a98289 100644 --- a/Zooper.Bee/Interfaces/IWorkflowValidation.cs +++ b/Zooper.Bee/Interfaces/IWorkflowValidation.cs @@ -1,28 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; -using Zooper.Fox; +using System; namespace Zooper.Bee.Interfaces; /// /// Base marker interface for all workflow validations /// -public interface IWorkflowValidation; +[Obsolete("Use IRailwayValidation instead. This interface will be removed in a future version.")] +public interface IWorkflowValidation : IRailwayValidation; /// /// Interface for a validation that validates a request and potentially returns an error /// /// The type of request being validated /// The type of error that might be returned -public interface IWorkflowValidation : IWorkflowValidation -{ - /// - /// Validates the request - /// - /// The request to validate - /// Cancellation token - /// An option containing an error if validation fails, or None if validation succeeds - Task> Validate( - TRequest request, - CancellationToken cancellationToken); -} \ No newline at end of file +[Obsolete("Use IRailwayValidation instead. This interface will be removed in a future version.")] +public interface IWorkflowValidation : IRailwayValidation; diff --git a/Zooper.Bee/Interfaces/IWorkflowValidations.cs b/Zooper.Bee/Interfaces/IWorkflowValidations.cs index 189513e..e45d26c 100644 --- a/Zooper.Bee/Interfaces/IWorkflowValidations.cs +++ b/Zooper.Bee/Interfaces/IWorkflowValidations.cs @@ -1,13 +1,17 @@ -namespace Zooper.Bee.Interfaces; +using System; + +namespace Zooper.Bee.Interfaces; /// /// Base marker interface for a collection of workflow validations /// -public interface IWorkflowValidations; +[Obsolete("Use IRailwayValidations instead. This interface will be removed in a future version.")] +public interface IWorkflowValidations : IRailwayValidations; /// /// Interface for a collection of workflow validations for a specific request and error type /// /// The type of request being validated /// The type of error that might be returned -public interface IWorkflowValidations : IWorkflowValidations; \ No newline at end of file +[Obsolete("Use IRailwayValidations instead. This interface will be removed in a future version.")] +public interface IWorkflowValidations : IRailwayValidations; diff --git a/Zooper.Bee/Internal/Branch.cs b/Zooper.Bee/Internal/Branch.cs index 70e00ed..2298ae5 100644 --- a/Zooper.Bee/Internal/Branch.cs +++ b/Zooper.Bee/Internal/Branch.cs @@ -11,7 +11,7 @@ namespace Zooper.Bee.Internal; internal sealed class Branch { public Func Condition { get; } - public List> Activities { get; } = []; + public List> Activities { get; } = []; public Branch(Func condition) { diff --git a/Zooper.Bee/Internal/ConditionalWorkflowStep.cs b/Zooper.Bee/Internal/ConditionalRailwayStep.cs similarity index 59% rename from Zooper.Bee/Internal/ConditionalWorkflowStep.cs rename to Zooper.Bee/Internal/ConditionalRailwayStep.cs index 0625ed1..3247b03 100644 --- a/Zooper.Bee/Internal/ConditionalWorkflowStep.cs +++ b/Zooper.Bee/Internal/ConditionalRailwayStep.cs @@ -3,19 +3,19 @@ namespace Zooper.Bee.Internal; /// -/// Represents a conditional step in the workflow that only executes if a condition is met. +/// Represents a conditional step in the railway that only executes if a condition is met. /// /// Type of the payload /// Type of the error -internal sealed class ConditionalWorkflowStep +internal sealed class ConditionalRailwayStep { private readonly Func _condition; - public WorkflowStep Activity { get; } + public RailwayStep Activity { get; } - public ConditionalWorkflowStep( + public ConditionalRailwayStep( Func condition, - WorkflowStep activity) + RailwayStep activity) { _condition = condition; Activity = activity; diff --git a/Zooper.Bee/Internal/Executors/ContextExecutor.cs b/Zooper.Bee/Internal/Executors/ContextExecutor.cs index 350a4c6..f1fd607 100644 --- a/Zooper.Bee/Internal/Executors/ContextExecutor.cs +++ b/Zooper.Bee/Internal/Executors/ContextExecutor.cs @@ -14,7 +14,7 @@ namespace Zooper.Bee.Internal.Executors; internal class ContextExecutor : IFeatureExecutor { /// - public bool CanExecute(Features.IWorkflowFeature feature) + public bool CanExecute(Features.IRailwayFeature feature) { if (feature == null) { @@ -33,7 +33,7 @@ public bool CanExecute(Features.IWorkflowFeature feature) /// public async Task> Execute( - Features.IWorkflowFeature feature, + Features.IRailwayFeature feature, TPayload payload, CancellationToken cancellationToken) { @@ -109,7 +109,7 @@ public async Task> Execute( /// The cancellation token /// Either the error or the modified payload private async Task> ExecuteTyped( - Features.IWorkflowFeature feature, + Features.IRailwayFeature feature, TPayload payload, CancellationToken cancellationToken) { diff --git a/Zooper.Bee/Internal/Executors/FeatureExecutorBase.cs b/Zooper.Bee/Internal/Executors/FeatureExecutorBase.cs index 0162b22..ac5bfd7 100644 --- a/Zooper.Bee/Internal/Executors/FeatureExecutorBase.cs +++ b/Zooper.Bee/Internal/Executors/FeatureExecutorBase.cs @@ -12,17 +12,17 @@ namespace Zooper.Bee.Internal.Executors; /// The type of the error /// The specific type of feature this executor handles internal abstract class FeatureExecutorBase : IFeatureExecutor - where TFeature : IWorkflowFeature + where TFeature : IRailwayFeature { /// - public bool CanExecute(IWorkflowFeature feature) + public bool CanExecute(IRailwayFeature feature) { return feature is TFeature; } /// public async Task> Execute( - IWorkflowFeature feature, + IRailwayFeature feature, TPayload payload, CancellationToken cancellationToken) { diff --git a/Zooper.Bee/Internal/Executors/FeatureExecutorFactory.cs b/Zooper.Bee/Internal/Executors/FeatureExecutorFactory.cs index 0f6c1bc..d0ae301 100644 --- a/Zooper.Bee/Internal/Executors/FeatureExecutorFactory.cs +++ b/Zooper.Bee/Internal/Executors/FeatureExecutorFactory.cs @@ -41,7 +41,7 @@ public FeatureExecutorFactory() /// The cancellation token /// Either the error or the modified payload public async Task> ExecuteFeature( - IWorkflowFeature feature, + IRailwayFeature feature, TPayload payload, CancellationToken cancellationToken) { diff --git a/Zooper.Bee/Internal/Executors/IFeatureExecutor.cs b/Zooper.Bee/Internal/Executors/IFeatureExecutor.cs index e6dd4e7..4a25b31 100644 --- a/Zooper.Bee/Internal/Executors/IFeatureExecutor.cs +++ b/Zooper.Bee/Internal/Executors/IFeatureExecutor.cs @@ -17,7 +17,7 @@ internal interface IFeatureExecutor /// /// The feature to check /// True if this executor can handle the feature, false otherwise - bool CanExecute(IWorkflowFeature feature); + bool CanExecute(IRailwayFeature feature); /// /// Executes the feature with the given payload @@ -27,7 +27,7 @@ internal interface IFeatureExecutor /// The cancellation token /// Either the error or the modified payload Task> Execute( - IWorkflowFeature feature, + IRailwayFeature feature, TPayload payload, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Zooper.Bee/Internal/Executors/ParallelExecutor.cs b/Zooper.Bee/Internal/Executors/ParallelExecutor.cs index a32d62a..be30d62 100644 --- a/Zooper.Bee/Internal/Executors/ParallelExecutor.cs +++ b/Zooper.Bee/Internal/Executors/ParallelExecutor.cs @@ -169,7 +169,7 @@ protected override async Task> ExecuteTyped( // Helper method to execute a group's activities private async Task> ExecuteGroupActivities( - List> activities, + List> activities, TPayload payload, CancellationToken cancellationToken) { diff --git a/Zooper.Bee/Internal/WorkflowGuard.cs b/Zooper.Bee/Internal/RailwayGuard.cs similarity index 80% rename from Zooper.Bee/Internal/WorkflowGuard.cs rename to Zooper.Bee/Internal/RailwayGuard.cs index a170a0e..1d7703a 100644 --- a/Zooper.Bee/Internal/WorkflowGuard.cs +++ b/Zooper.Bee/Internal/RailwayGuard.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Zooper.Fox; @@ -6,16 +6,16 @@ namespace Zooper.Bee.Internal; /// -/// Represents a guard that checks if a workflow can be executed. +/// Represents a guard that checks if a railway can be executed. /// /// Type of the request /// Type of the error -internal sealed class WorkflowGuard +internal sealed class RailwayGuard { private readonly Func>> _condition; private readonly string? _name; - public WorkflowGuard( + public RailwayGuard( Func>> condition, string? name = null) { @@ -29,4 +29,4 @@ public Task> Check( { return _condition(request, token); } -} \ No newline at end of file +} diff --git a/Zooper.Bee/Internal/WorkflowStep.cs b/Zooper.Bee/Internal/RailwayStep.cs similarity index 82% rename from Zooper.Bee/Internal/WorkflowStep.cs rename to Zooper.Bee/Internal/RailwayStep.cs index 779f75b..355ed2f 100644 --- a/Zooper.Bee/Internal/WorkflowStep.cs +++ b/Zooper.Bee/Internal/RailwayStep.cs @@ -6,16 +6,16 @@ namespace Zooper.Bee.Internal; /// -/// Represents a step in the workflow that operates on a payload. +/// Represents a step in the railway that operates on a payload. /// /// Type of the payload /// Type of the error -internal sealed class WorkflowStep +internal sealed class RailwayStep { private readonly Func>> _activity; private readonly string? _name; - public WorkflowStep( + public RailwayStep( Func>> activity, string? name = null) { diff --git a/Zooper.Bee/Internal/WorkflowValidation.cs b/Zooper.Bee/Internal/RailwayValidation.cs similarity index 89% rename from Zooper.Bee/Internal/WorkflowValidation.cs rename to Zooper.Bee/Internal/RailwayValidation.cs index db20eb4..2ddabbf 100644 --- a/Zooper.Bee/Internal/WorkflowValidation.cs +++ b/Zooper.Bee/Internal/RailwayValidation.cs @@ -10,12 +10,12 @@ namespace Zooper.Bee.Internal; /// /// Type of the request /// Type of the error -internal sealed class WorkflowValidation +internal sealed class RailwayValidation { private readonly Func>> _validation; private readonly string? _name; - public WorkflowValidation( + public RailwayValidation( Func>> validation, string? name = null) { @@ -27,4 +27,4 @@ public Task> Validate(TRequest request, CancellationToken token) { return _validation(request, token); } -} \ No newline at end of file +} diff --git a/Zooper.Bee/Railway.cs b/Zooper.Bee/Railway.cs new file mode 100644 index 0000000..bdd7f6e --- /dev/null +++ b/Zooper.Bee/Railway.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee; + +/// +/// Represents a railway that processes a request and either succeeds with a result of type +/// or fails with an error of type . +/// +/// The type of the request +/// The type of the success result +/// The type of the error result +public sealed class Railway +{ + private readonly Func>> _executor; + + internal Railway(Func>> executor) + { + _executor = executor; + } + + /// + /// Executes the railway with the specified request. + /// + /// The request to process + /// A cancellation token to abort the operation + /// + /// A task that represents the asynchronous operation. The task result contains + /// either a success value of type or an error of type . + /// + public Task> Execute(TRequest request, CancellationToken cancellationToken = default) + { + return _executor(request, cancellationToken); + } +} diff --git a/Zooper.Bee/RailwayBuilder.cs b/Zooper.Bee/RailwayBuilder.cs new file mode 100644 index 0000000..c21a7c1 --- /dev/null +++ b/Zooper.Bee/RailwayBuilder.cs @@ -0,0 +1,858 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Zooper.Bee.Features; +using Zooper.Bee.Internal; +using Zooper.Bee.Internal.Executors; +using Zooper.Fox; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Zooper.Bee; + +/// +/// Represents a builder for a railway that processes a request and +/// either succeeds with a result +/// or fails with a . +/// +/// The type of the request input. +/// The type of the payload used to carry intermediate data. +/// The type of the success result. +/// The type of the error result. +/// +/// Initializes a new instance of the class. +/// +public class RailwayBuilder +{ + private readonly Func _contextFactory; + private readonly Func _resultSelector; + + private readonly List> _guards = []; + private readonly List> _validations = []; + private readonly List> _activities = []; + private readonly List> _conditionalActivities = []; + private readonly List> _finallyActivities = []; + private readonly List> _branches = []; + private readonly List _branchesWithLocalPayload = []; + + // Collections for new features + private readonly List> _features = []; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Factory function that takes a request of type + /// and produces a context of type . + /// + /// + /// Selector function that converts the final + /// into a success result of type . + /// + public RailwayBuilder( + Func contextFactory, + Func resultSelector) + { + _contextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); + _resultSelector = resultSelector ?? throw new ArgumentNullException(nameof(resultSelector)); + } + + /// + /// Adds a validation rule to the railway. + /// + /// The validation function + /// The builder instance for method chaining + public RailwayBuilder Validate( + Func>> validation) + { + _validations.Add(new(validation)); + return this; + } + + /// + /// Adds a synchronous validation rule to the railway. + /// + /// The validation function + /// The builder instance for method chaining + public RailwayBuilder Validate(Func> validation) + { + _validations.Add( + new(( + request, + _) => Task.FromResult(validation(request)) + ) + ); + return this; + } + + /// + /// Adds a guard to check if the railway can be executed. + /// Guards are evaluated before any validations or activities. + /// If a guard fails, the railway will not execute and will return the error. + /// + /// The guard function that returns Either an error or Unit + /// The builder instance for method chaining + public RailwayBuilder Guard( + Func>> guard) + { + _guards.Add(new(guard)); + return this; + } + + /// + /// Adds a synchronous guard to check if the railway can be executed. + /// + /// The guard function that returns Either an error or Unit + /// The builder instance for method chaining + public RailwayBuilder Guard(Func> guard) + { + _guards.Add( + new(( + request, + _) => Task.FromResult(guard(request)) + ) + ); + return this; + } + + /// + /// Adds an activity to the railway. + /// + /// The activity function + /// The builder instance for method chaining + public RailwayBuilder Do( + Func>> activity) + { + _activities.Add(new(activity)); + return this; + } + + /// + /// Adds a synchronous activity to the railway. + /// + /// The activity function + /// The builder instance for method chaining + public RailwayBuilder Do(Func> activity) + { + _activities.Add( + new(( + payload, + _) => Task.FromResult(activity(payload)) + ) + ); + return this; + } + + /// + /// Adds multiple activities to the railway. + /// + /// The activity functions + /// The builder instance for method chaining + public RailwayBuilder DoAll( + params Func>>[] activities) + { + foreach (var activity in activities) + { + _activities.Add(new(activity)); + } + + return this; + } + + /// + /// Adds multiple synchronous activities to the railway. + /// + /// The activity functions + /// The builder instance for method chaining + public RailwayBuilder DoAll(params Func>[] activities) + { + foreach (var activity in activities) + { + _activities.Add( + new(( + payload, + _) => Task.FromResult(activity(payload)) + ) + ); + } + + return this; + } + + /// + /// Adds a conditional activity to the railway that will only execute if the condition returns true. + /// + /// The condition to evaluate + /// The activity to execute if the condition is true + /// The builder instance for method chaining + public RailwayBuilder DoIf( + Func condition, + Func>> activity) + { + _conditionalActivities.Add( + new( + condition, + new(activity) + ) + ); + return this; + } + + /// + /// Adds a synchronous conditional activity to the railway that will only execute if the condition returns true. + /// + /// The condition to evaluate + /// The activity to execute if the condition is true + /// The builder instance for method chaining + public RailwayBuilder DoIf( + Func condition, + Func> activity) + { + _conditionalActivities.Add( + new( + condition, + new(( + payload, + _) => Task.FromResult(activity(payload)) + ) + ) + ); + return this; + } + + /// + /// Creates a branch in the railway that will only execute if the condition is true. + /// + /// The condition to evaluate + /// A branch builder that allows adding activities to the branch + [Obsolete("Use Group() method instead. This method will be removed in a future version.")] + public BranchBuilder Branch(Func condition) + { + var branch = new Branch(condition); + _branches.Add(branch); + return new(this, branch); + } + + /// + /// Creates a branch in the railway that will only execute if the condition is true. + /// + /// The condition to evaluate + /// An action that configures the branch + /// The railway builder to continue the railway definition + [Obsolete("Use Group() method instead. This method will be removed in a future version.")] + public RailwayBuilder Branch( + Func condition, + Action> branchConfiguration) + { + var branch = new Branch(condition); + _branches.Add(branch); + var branchBuilder = new BranchBuilder(this, branch); + branchConfiguration(branchBuilder); + return this; + } + + /// + /// Creates an unconditional branch in the railway. (Always executes) + /// + /// An action that configures the branch + /// The railway builder to continue the railway definition + [Obsolete("Use Group() method instead. This method will be removed in a future version.")] + public RailwayBuilder Branch( + Action> branchConfiguration) + { + return Branch(_ => true, branchConfiguration); + } + + /// + /// Creates a group of activities in the railway with an optional condition. + /// + /// The condition to evaluate. If null, the group always executes. + /// An action that configures the group + /// The railway builder to continue the railway definition + public RailwayBuilder Group( + Func? condition, + Action> groupConfiguration) + { + var group = new Features.Group.Group(condition); + _features.Add(group); + var groupBuilder = new Features.Group.GroupBuilder(this, group); + groupConfiguration(groupBuilder); + return this; + } + + /// + /// Creates a group of activities in the railway that always executes. + /// + /// An action that configures the group + /// The railway builder to continue the railway definition + public RailwayBuilder Group( + Action> groupConfiguration) + { + return Group(null, groupConfiguration); + } + + /// + /// Creates a branch in the railway with a local payload that will only execute if the condition is true. + /// + /// The type of the local branch payload + /// The condition to evaluate + /// The factory function that creates the local payload + /// A branch builder that allows adding activities to the branch + [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] + public BranchWithLocalPayloadBuilder BranchWithLocalPayload( + Func condition, + Func localPayloadFactory) + { + var branch = new BranchWithLocalPayload(condition, localPayloadFactory); + _branchesWithLocalPayload.Add(branch); + return new(branch); + } + + /// + /// Creates a branch in the railway with a local payload that will only execute if the condition is true. + /// + /// The type of the local branch payload + /// The condition to evaluate + /// The factory function that creates the local payload + /// An action that configures the branch + /// The railway builder to continue the railway definition + [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] + public RailwayBuilder BranchWithLocalPayload( + Func condition, + Func localPayloadFactory, + Action> branchConfiguration) + { + var branch = new BranchWithLocalPayload(condition, localPayloadFactory); + _branchesWithLocalPayload.Add(branch); + var branchBuilder = new BranchWithLocalPayloadBuilder(branch); + branchConfiguration(branchBuilder); + return this; + } + + /// + /// Creates a branch in the railway with a local payload that always executes. + /// This is a convenience method for organizing related activities. + /// + /// The type of the local branch payload + /// The factory function that creates the local payload + /// A branch builder that allows adding activities to the branch + [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] + public BranchWithLocalPayloadBuilder BranchWithLocalPayload( + Func localPayloadFactory) + { + var branch = new BranchWithLocalPayload(_ => true, localPayloadFactory); + _branchesWithLocalPayload.Add(branch); + return new(branch); + } + + /// + /// Creates a branch in the railway with a local payload that always executes. + /// This is a convenience method for organizing related activities. + /// + /// The type of the local branch payload + /// The factory function that creates the local payload + /// An action that configures the branch + /// The railway builder to continue the railway definition + [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] + public RailwayBuilder BranchWithLocalPayload( + Func localPayloadFactory, + Action> branchConfiguration) + { + return BranchWithLocalPayload(_ => true, localPayloadFactory, branchConfiguration); + } + + /// + /// Creates a context with local state in the railway and an optional condition. + /// + /// The type of the local context state + /// The condition to evaluate. If null, the context always executes. + /// The factory function that creates the local state + /// An action that configures the context + /// The railway builder to continue the railway definition + public RailwayBuilder WithContext( + Func? condition, + Func localStateFactory, + Action> contextConfiguration) + { + var context = new Features.Context.Context(condition, localStateFactory); + _features.Add(context); + var contextBuilder = new Features.Context.ContextBuilder(this, context); + contextConfiguration(contextBuilder); + return this; + } + + /// + /// Creates a context with local state in the railway that always executes. + /// + /// The type of the local context state + /// The factory function that creates the local state + /// An action that configures the context + /// The railway builder to continue the railway definition + public RailwayBuilder WithContext( + Func localStateFactory, + Action> contextConfiguration) + { + return WithContext(null, localStateFactory, contextConfiguration); + } + + /// + /// Creates a detached group of activities in the railway with an optional condition. + /// Detached groups don't merge their results back into the main railway. + /// + /// The condition to evaluate. If null, the detached group always executes. + /// An action that configures the detached group + /// The railway builder to continue the railway definition + public RailwayBuilder Detach( + Func? condition, + Action> detachedConfiguration) + { + var detached = new Features.Detached.Detached(condition); + _features.Add(detached); + var detachedBuilder = new Features.Detached.DetachedBuilder(this, detached); + detachedConfiguration(detachedBuilder); + return this; + } + + /// + /// Creates a detached group of activities in the railway that always executes. + /// Detached groups don't merge their results back into the main railway. + /// + /// An action that configures the detached group + /// The railway builder to continue the railway definition + public RailwayBuilder Detach( + Action> detachedConfiguration) + { + return Detach(null, detachedConfiguration); + } + + /// + /// 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 railway. + /// + /// The condition to evaluate. If null, the parallel execution always occurs. + /// An action that configures the parallel execution + /// The railway builder to continue the railway definition + public RailwayBuilder Parallel( + Func? condition, + Action> parallelConfiguration) + { + var parallel = new Features.Parallel.Parallel(condition); + _features.Add(parallel); + var parallelBuilder = new Features.Parallel.ParallelBuilder(this, parallel); + parallelConfiguration(parallelBuilder); + return this; + } + + /// + /// Creates a parallel execution of multiple groups that always executes. + /// All groups execute in parallel, and their results are merged back into the main railway. + /// + /// An action that configures the parallel execution + /// The railway builder to continue the railway definition + public RailwayBuilder Parallel( + Action> parallelConfiguration) + { + return Parallel(null, parallelConfiguration); + } + + /// + /// 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. + /// + /// The condition to evaluate. If null, the parallel detached execution always occurs. + /// An action that configures the parallel detached execution + /// The railway builder to continue the railway definition + public RailwayBuilder ParallelDetached( + Func? condition, + Action> parallelDetachedConfiguration) + { + var parallelDetached = new Features.Parallel.ParallelDetached(condition); + _features.Add(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. + /// + /// An action that configures the parallel detached execution + /// The railway builder to continue the railway definition + public RailwayBuilder ParallelDetached( + Action> parallelDetachedConfiguration) + { + return ParallelDetached(null, parallelDetachedConfiguration); + } + + /// + /// Adds an activity to the "finally" block that will always execute, even if the railway fails. + /// + /// The activity to execute + /// The builder instance for method chaining + public RailwayBuilder Finally( + Func>> activity) + { + _finallyActivities.Add(new(activity)); + return this; + } + + /// + /// Adds a synchronous activity to the "finally" block that will always execute, even if the railway fails. + /// + /// The activity to execute + /// The builder instance for method chaining + public RailwayBuilder Finally(Func> activity) + { + _finallyActivities.Add( + new(( + payload, + _) => Task.FromResult(activity(payload)) + ) + ); + return this; + } + + /// + /// Builds a railway that processes a request and returns either a success or an error. + /// + public Railway Build() + { + return new(ExecuteRailwayAsync); + } + + /// + /// Executes the railway: runs validations, activities, conditional logic, branches, features, and finally actions. + /// + /// The railway 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> ExecuteRailwayAsync( + TRequest request, + CancellationToken cancellationToken) + { + // We run the validations first to ensure the request is valid before proceeding + var validationResult = await RunValidationsAsync(request, cancellationToken); + if (validationResult.IsLeft) + return Either.FromLeft(validationResult.Left!); + + var guardResult = await RunGuardsAsync(request, cancellationToken); + if (guardResult.IsLeft) + return Either.FromLeft(guardResult.Left!); + + var payload = _contextFactory(request); + if (payload == null) + return Either.FromRight(_resultSelector(default!)); + + try + { + var activitiesResult = await RunActivitiesAsync(payload, cancellationToken); + if (activitiesResult.IsLeft) + return Either.FromLeft(activitiesResult.Left!); + + payload = activitiesResult.Right!; + + var conditionalResult = await RunConditionalActivitiesAsync(payload, cancellationToken); + if (conditionalResult.IsLeft) + return Either.FromLeft(conditionalResult.Left!); + + 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 + { + _ = await RunFinallyActivitiesAsync(payload, cancellationToken); + } + } + + /// + /// Runs all configured validations against the request. + /// + /// The railway 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) + { + var validationOption = await validation.Validate(request, cancellationToken); + if (validationOption.IsSome && validationOption.Value != null) + return Either.FromLeft(validationOption.Value); + } + + return Either.FromRight(default!); + } + + /// + /// Runs all configured guards against the request. + /// + /// The railway request. + /// Token to observe for cancellation. + /// + /// An Either with Left if any guard fails, or Right with Unit on success. + /// + private async Task> RunGuardsAsync( + TRequest request, + CancellationToken cancellationToken) + { + foreach (var guard in _guards) + { + var result = await guard.Check(request, cancellationToken); + if (result.IsLeft && result.Left != null) + return Either.FromLeft(result.Left); + } + + return Either.FromRight(Unit.Value); + } + + /// + /// 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) + { + var result = await activity.Execute(payload, cancellationToken); + if (result.IsLeft && result.Left != null) + return Either.FromLeft(result.Left); + + payload = result.Right!; + } + + return Either.FromRight(payload); + } + + /// + /// 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) + { + foreach (var conditionalActivity in _conditionalActivities) + { + if (!conditionalActivity.ShouldExecute(payload)) + continue; + + var result = await conditionalActivity.Activity.Execute(payload, cancellationToken); + if (result.IsLeft && result.Left != null) + return Either.FromLeft(result.Left); + + payload = result.Right!; + } + + 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) + { + 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!; + } + } + + 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) + { + var result = await ExecuteBranchWithLocalPayloadDynamic(branchObject, payload, cancellationToken); + if (result.IsLeft && result.Left != null) + return Either.FromLeft(result.Left); + + payload = result.Right!; + } + + return Either.FromRight(payload); + } + + /// + /// Executes railway 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) + { + 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!; + } + + 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 railway 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<,,>)) + { + var methodInfo = typeof(RailwayBuilder) + .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); + } + + 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 railway 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); + + var localPayload = branch.LocalPayloadFactory(payload); + + foreach (var activity in branch.Activities) + { + var result = await activity.Execute(payload, localPayload, cancellationToken); + if (result.IsLeft) + return Either.FromLeft(result.Left); + + (payload, localPayload) = result.Right; + } + + return Either.FromRight(payload); + } +} diff --git a/Zooper.Bee/RailwayBuilderFactory.cs b/Zooper.Bee/RailwayBuilderFactory.cs new file mode 100644 index 0000000..a46e291 --- /dev/null +++ b/Zooper.Bee/RailwayBuilderFactory.cs @@ -0,0 +1,51 @@ +using System; +using Zooper.Fox; + +namespace Zooper.Bee; + +/// +/// Provides factory methods for creating railways without requiring a request parameter. +/// +public static class RailwayBuilderFactory +{ + /// + /// Creates a new railway builder that doesn't require a request parameter. + /// + /// The type of payload that will be used throughout the railway + /// The type of the success result + /// The type of the error result + /// A factory function that creates the initial payload + /// A function that creates the success result from the final payload + /// A railway builder instance + public static RailwayBuilder Create( + Func payloadFactory, + Func resultSelector) + { + return new RailwayBuilder( + _ => payloadFactory(), + resultSelector); + } + + /// + /// Creates a new railway that doesn't require a request parameter. + /// + /// The type of payload that will be used throughout the railway + /// The type of the success result + /// The type of the error result + /// A factory function that creates the initial payload + /// A function that creates the success result from the final payload + /// An action that configures the railway + /// A railway instance + public static Railway CreateRailway( + Func payloadFactory, + Func resultSelector, + Action> configure) + { + var builder = new RailwayBuilder( + _ => payloadFactory(), + resultSelector); + + configure(builder); + return builder.Build(); + } +} diff --git a/Zooper.Bee/Workflow.cs b/Zooper.Bee/Workflow.cs index e9049ed..52f1f94 100644 --- a/Zooper.Bee/Workflow.cs +++ b/Zooper.Bee/Workflow.cs @@ -12,13 +12,14 @@ namespace Zooper.Bee; /// The type of the request /// The type of the success result /// The type of the error result +[Obsolete("Use Railway instead. This class will be removed in a future version.")] public sealed class Workflow { - private readonly Func>> _executor; + private readonly Railway _inner; - internal Workflow(Func>> executor) + internal Workflow(Railway inner) { - _executor = executor; + _inner = inner; } /// @@ -32,6 +33,14 @@ internal Workflow(Func public Task> Execute(TRequest request, CancellationToken cancellationToken = default) { - return _executor(request, cancellationToken); + return _inner.Execute(request, cancellationToken); } -} \ No newline at end of file + + /// + /// Implicit conversion to Railway for seamless migration. + /// + public static implicit operator Railway(Workflow workflow) + { + return workflow._inner; + } +} diff --git a/Zooper.Bee/WorkflowBuilder.cs b/Zooper.Bee/WorkflowBuilder.cs index 1bfae4c..fa34a57 100644 --- a/Zooper.Bee/WorkflowBuilder.cs +++ b/Zooper.Bee/WorkflowBuilder.cs @@ -1,14 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Zooper.Bee.Features; -using Zooper.Bee.Internal; -using Zooper.Bee.Internal.Executors; -using Zooper.Fox; - -// ReSharper disable MemberCanBePrivate.Global +using System; namespace Zooper.Bee; @@ -21,25 +11,10 @@ namespace Zooper.Bee; /// The type of the payload used to carry intermediate data. /// The type of the success result. /// The type of the error result. -/// -/// Initializes a new instance of the class. -/// +[Obsolete("Use RailwayBuilder instead. This class will be removed in a future version.")] public sealed class WorkflowBuilder + : RailwayBuilder { - private readonly Func _contextFactory; - private readonly Func _resultSelector; - - private readonly List> _guards = []; - private readonly List> _validations = []; - private readonly List> _activities = []; - private readonly List> _conditionalActivities = []; - private readonly List> _finallyActivities = []; - private readonly List> _branches = []; - private readonly List _branchesWithLocalPayload = []; - - // Collections for new features - private readonly List> _features = []; - /// /// Initializes a new instance of the class. /// @@ -54,805 +29,7 @@ public sealed class WorkflowBuilder public WorkflowBuilder( Func contextFactory, Func resultSelector) + : base(contextFactory, resultSelector) { - _contextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); - _resultSelector = resultSelector ?? throw new ArgumentNullException(nameof(resultSelector)); - } - - /// - /// Adds a validation rule to the workflow. - /// - /// The validation function - /// The builder instance for method chaining - public WorkflowBuilder Validate( - Func>> validation) - { - _validations.Add(new(validation)); - return this; - } - - /// - /// Adds a synchronous validation rule to the workflow. - /// - /// The validation function - /// The builder instance for method chaining - public WorkflowBuilder Validate(Func> validation) - { - _validations.Add( - new(( - request, - _) => Task.FromResult(validation(request)) - ) - ); - return this; - } - - /// - /// Adds a guard to check if the workflow can be executed. - /// Guards are evaluated before any validations or activities. - /// If a guard fails, the workflow will not execute and will return the error. - /// - /// The guard function that returns Either an error or Unit - /// The builder instance for method chaining - public WorkflowBuilder Guard( - Func>> guard) - { - _guards.Add(new(guard)); - return this; - } - - /// - /// Adds a synchronous guard to check if the workflow can be executed. - /// - /// The guard function that returns Either an error or Unit - /// The builder instance for method chaining - public WorkflowBuilder Guard(Func> guard) - { - _guards.Add( - new(( - request, - _) => Task.FromResult(guard(request)) - ) - ); - return this; - } - - /// - /// Adds an activity to the workflow. - /// - /// The activity function - /// The builder instance for method chaining - public WorkflowBuilder Do( - Func>> activity) - { - _activities.Add(new(activity)); - return this; - } - - /// - /// Adds a synchronous activity to the workflow. - /// - /// The activity function - /// The builder instance for method chaining - public WorkflowBuilder Do(Func> activity) - { - _activities.Add( - new(( - payload, - _) => Task.FromResult(activity(payload)) - ) - ); - return this; - } - - /// - /// Adds multiple activities to the workflow. - /// - /// The activity functions - /// The builder instance for method chaining - public WorkflowBuilder DoAll( - params Func>>[] activities) - { - foreach (var activity in activities) - { - _activities.Add(new(activity)); - } - - return this; - } - - /// - /// Adds multiple synchronous activities to the workflow. - /// - /// The activity functions - /// The builder instance for method chaining - public WorkflowBuilder DoAll(params Func>[] activities) - { - foreach (var activity in activities) - { - _activities.Add( - new(( - payload, - _) => Task.FromResult(activity(payload)) - ) - ); - } - - return this; - } - - /// - /// Adds a conditional activity to the workflow that will only execute if the condition returns true. - /// - /// The condition to evaluate - /// The activity to execute if the condition is true - /// The builder instance for method chaining - public WorkflowBuilder DoIf( - Func condition, - Func>> activity) - { - _conditionalActivities.Add( - new( - condition, - new(activity) - ) - ); - return this; - } - - /// - /// Adds a synchronous conditional activity to the workflow that will only execute if the condition returns true. - /// - /// The condition to evaluate - /// The activity to execute if the condition is true - /// The builder instance for method chaining - public WorkflowBuilder DoIf( - Func condition, - Func> activity) - { - _conditionalActivities.Add( - new( - condition, - new(( - payload, - _) => Task.FromResult(activity(payload)) - ) - ) - ); - return this; - } - - /// - /// Creates a branch in the workflow that will only execute if the condition is true. - /// - /// The condition to evaluate - /// A branch builder that allows adding activities to the branch - [Obsolete("Use Group() method instead. This method will be removed in a future version.")] - public BranchBuilder Branch(Func condition) - { - var branch = new Branch(condition); - _branches.Add(branch); - return new(this, branch); - } - - /// - /// Creates a branch in the workflow that will only execute if the condition is true. - /// - /// The condition to evaluate - /// An action that configures the branch - /// The workflow builder to continue the workflow definition - [Obsolete("Use Group() method instead. This method will be removed in a future version.")] - public WorkflowBuilder Branch( - Func condition, - Action> branchConfiguration) - { - var branch = new Branch(condition); - _branches.Add(branch); - var branchBuilder = new BranchBuilder(this, branch); - branchConfiguration(branchBuilder); - return this; - } - - /// - /// Creates an unconditional branch in the workflow. (Always executes) - /// - /// An action that configures the branch - /// The workflow builder to continue the workflow definition - [Obsolete("Use Group() method instead. This method will be removed in a future version.")] - public WorkflowBuilder Branch( - Action> branchConfiguration) - { - return Branch(_ => true, branchConfiguration); - } - - /// - /// Creates a group of activities in the workflow with an optional condition. - /// - /// The condition to evaluate. If null, the group always executes. - /// An action that configures the group - /// The workflow builder to continue the workflow definition - public WorkflowBuilder Group( - Func? condition, - Action> groupConfiguration) - { - var group = new Features.Group.Group(condition); - _features.Add(group); - var groupBuilder = new Features.Group.GroupBuilder(this, group); - groupConfiguration(groupBuilder); - return this; - } - - /// - /// Creates a group of activities in the workflow that always executes. - /// - /// An action that configures the group - /// The workflow builder to continue the workflow definition - public WorkflowBuilder Group( - Action> groupConfiguration) - { - return Group(null, groupConfiguration); - } - - /// - /// Creates a branch in the workflow with a local payload that will only execute if the condition is true. - /// - /// The type of the local branch payload - /// The condition to evaluate - /// The factory function that creates the local payload - /// A branch builder that allows adding activities to the branch - [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] - public BranchWithLocalPayloadBuilder BranchWithLocalPayload( - Func condition, - Func localPayloadFactory) - { - var branch = new BranchWithLocalPayload(condition, localPayloadFactory); - _branchesWithLocalPayload.Add(branch); - return new(branch); - } - - /// - /// Creates a branch in the workflow with a local payload that will only execute if the condition is true. - /// - /// The type of the local branch payload - /// The condition to evaluate - /// The factory function that creates the local payload - /// An action that configures the branch - /// The workflow builder to continue the workflow definition - [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] - public WorkflowBuilder BranchWithLocalPayload( - Func condition, - Func localPayloadFactory, - Action> branchConfiguration) - { - var branch = new BranchWithLocalPayload(condition, localPayloadFactory); - _branchesWithLocalPayload.Add(branch); - var branchBuilder = new BranchWithLocalPayloadBuilder(branch); - branchConfiguration(branchBuilder); - return this; - } - - /// - /// Creates a branch in the workflow with a local payload that always executes. - /// This is a convenience method for organizing related activities. - /// - /// The type of the local branch payload - /// The factory function that creates the local payload - /// A branch builder that allows adding activities to the branch - [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] - public BranchWithLocalPayloadBuilder BranchWithLocalPayload( - Func localPayloadFactory) - { - var branch = new BranchWithLocalPayload(_ => true, localPayloadFactory); - _branchesWithLocalPayload.Add(branch); - return new(branch); - } - - /// - /// Creates a branch in the workflow with a local payload that always executes. - /// This is a convenience method for organizing related activities. - /// - /// The type of the local branch payload - /// The factory function that creates the local payload - /// An action that configures the branch - /// The workflow builder to continue the workflow definition - [Obsolete("Use WithContext() method instead. This method will be removed in a future version.")] - public WorkflowBuilder BranchWithLocalPayload( - Func localPayloadFactory, - Action> branchConfiguration) - { - return BranchWithLocalPayload(_ => true, localPayloadFactory, branchConfiguration); - } - - /// - /// Creates a context with local state in the workflow and an optional condition. - /// - /// The type of the local context state - /// The condition to evaluate. If null, the context always executes. - /// The factory function that creates the local state - /// An action that configures the context - /// The workflow builder to continue the workflow definition - public WorkflowBuilder WithContext( - Func? condition, - Func localStateFactory, - Action> contextConfiguration) - { - var context = new Features.Context.Context(condition, localStateFactory); - _features.Add(context); - var contextBuilder = new Features.Context.ContextBuilder(this, context); - contextConfiguration(contextBuilder); - return this; - } - - /// - /// Creates a context with local state in the workflow that always executes. - /// - /// The type of the local context state - /// The factory function that creates the local state - /// An action that configures the context - /// The workflow builder to continue the workflow definition - public WorkflowBuilder WithContext( - Func localStateFactory, - Action> contextConfiguration) - { - return WithContext(null, localStateFactory, contextConfiguration); - } - - /// - /// Creates a detached group of activities in the workflow with an optional condition. - /// Detached groups don't merge their results back into the main workflow. - /// - /// The condition to evaluate. If null, the detached group always executes. - /// An action that configures the detached group - /// The workflow builder to continue the workflow definition - public WorkflowBuilder Detach( - Func? condition, - Action> detachedConfiguration) - { - var detached = new Features.Detached.Detached(condition); - _features.Add(detached); - var detachedBuilder = new Features.Detached.DetachedBuilder(this, detached); - detachedConfiguration(detachedBuilder); - return this; - } - - /// - /// Creates a detached group of activities in the workflow that always executes. - /// Detached groups don't merge their results back into the main workflow. - /// - /// An action that configures the detached group - /// The workflow builder to continue the workflow definition - public WorkflowBuilder Detach( - Action> detachedConfiguration) - { - return Detach(null, detachedConfiguration); - } - - /// - /// 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. - /// - /// The condition to evaluate. If null, the parallel execution always occurs. - /// An action that configures the parallel execution - /// The workflow builder to continue the workflow definition - public WorkflowBuilder Parallel( - Func? condition, - Action> parallelConfiguration) - { - var parallel = new Features.Parallel.Parallel(condition); - _features.Add(parallel); - var parallelBuilder = new Features.Parallel.ParallelBuilder(this, parallel); - parallelConfiguration(parallelBuilder); - return this; - } - - /// - /// 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. - /// - /// An action that configures the parallel execution - /// The workflow builder to continue the workflow definition - public WorkflowBuilder Parallel( - Action> parallelConfiguration) - { - return Parallel(null, parallelConfiguration); - } - - /// - /// 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. - /// - /// The condition to evaluate. If null, the parallel detached execution always occurs. - /// An action that configures the parallel detached execution - /// The workflow builder to continue the workflow definition - public WorkflowBuilder ParallelDetached( - Func? condition, - Action> parallelDetachedConfiguration) - { - var parallelDetached = new Features.Parallel.ParallelDetached(condition); - _features.Add(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. - /// - /// An action that configures the parallel detached execution - /// The workflow builder to continue the workflow definition - public WorkflowBuilder ParallelDetached( - Action> parallelDetachedConfiguration) - { - return ParallelDetached(null, parallelDetachedConfiguration); - } - - /// - /// 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(activity)); - return this; - } - - /// - /// 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) - { - _finallyActivities.Add( - new(( - payload, - _) => Task.FromResult(activity(payload)) - ) - ); - return this; - } - - /// - /// Builds a workflow that processes a request and returns either a success or an error. - /// - public Workflow Build() - { - return new(ExecuteWorkflowAsync); - } - - /// - /// 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) - { - // We run the validations first to ensure the request is valid before proceeding - var validationResult = await RunValidationsAsync(request, cancellationToken); - if (validationResult.IsLeft) - return Either.FromLeft(validationResult.Left!); - - var guardResult = await RunGuardsAsync(request, cancellationToken); - if (guardResult.IsLeft) - return Either.FromLeft(guardResult.Left!); - - var payload = _contextFactory(request); - if (payload == null) - return Either.FromRight(_resultSelector(default!)); - - try - { - var activitiesResult = await RunActivitiesAsync(payload, cancellationToken); - if (activitiesResult.IsLeft) - return Either.FromLeft(activitiesResult.Left!); - - payload = activitiesResult.Right!; - - var conditionalResult = await RunConditionalActivitiesAsync(payload, cancellationToken); - if (conditionalResult.IsLeft) - return Either.FromLeft(conditionalResult.Left!); - - 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 - { - _ = await RunFinallyActivitiesAsync(payload, cancellationToken); - } - } - - /// - /// 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) - { - var validationOption = await validation.Validate(request, cancellationToken); - if (validationOption.IsSome && validationOption.Value != null) - return Either.FromLeft(validationOption.Value); - } - - return Either.FromRight(default!); - } - - /// - /// Runs all configured guards against the request. - /// - /// The workflow request. - /// Token to observe for cancellation. - /// - /// An Either with Left if any guard fails, or Right with Unit on success. - /// - private async Task> RunGuardsAsync( - TRequest request, - CancellationToken cancellationToken) - { - foreach (var guard in _guards) - { - var result = await guard.Check(request, cancellationToken); - if (result.IsLeft && result.Left != null) - return Either.FromLeft(result.Left); - } - - return Either.FromRight(Unit.Value); - } - - /// - /// 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) - { - var result = await activity.Execute(payload, cancellationToken); - if (result.IsLeft && result.Left != null) - return Either.FromLeft(result.Left); - - payload = result.Right!; - } - - return Either.FromRight(payload); - } - - /// - /// 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) - { - foreach (var conditionalActivity in _conditionalActivities) - { - if (!conditionalActivity.ShouldExecute(payload)) - continue; - - var result = await conditionalActivity.Activity.Execute(payload, cancellationToken); - if (result.IsLeft && result.Left != null) - return Either.FromLeft(result.Left); - - payload = result.Right!; - } - - 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) - { - 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!; - } - } - - 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) - { - var result = await ExecuteBranchWithLocalPayloadDynamic(branchObject, payload, cancellationToken); - if (result.IsLeft && result.Left != null) - return Either.FromLeft(result.Left); - - payload = result.Right!; - } - - 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) - { - 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!; - } - - 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<,,>)) - { - 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); - } - - 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); - - var localPayload = branch.LocalPayloadFactory(payload); - - foreach (var activity in branch.Activities) - { - var result = await activity.Execute(payload, localPayload, cancellationToken); - if (result.IsLeft) - return Either.FromLeft(result.Left); - - (payload, localPayload) = result.Right; - } - - return Either.FromRight(payload); } -} \ No newline at end of file +} diff --git a/Zooper.Bee/WorkflowBuilderFactory.cs b/Zooper.Bee/WorkflowBuilderFactory.cs index 00aa103..1c1ffd9 100644 --- a/Zooper.Bee/WorkflowBuilderFactory.cs +++ b/Zooper.Bee/WorkflowBuilderFactory.cs @@ -6,6 +6,7 @@ namespace Zooper.Bee; /// /// Provides factory methods for creating workflows without requiring a request parameter. /// +[Obsolete("Use RailwayBuilderFactory instead. This class will be removed in a future version.")] public static class WorkflowBuilderFactory { /// @@ -16,14 +17,13 @@ public static class WorkflowBuilderFactory /// The type of the error result /// A factory function that creates the initial payload /// A function that creates the success result from the final payload - /// A workflow builder instance - public static WorkflowBuilder Create( + /// A railway builder instance + [Obsolete("Use RailwayBuilderFactory.Create instead. This method will be removed in a future version.")] + public static RailwayBuilder Create( Func payloadFactory, Func resultSelector) { - return new WorkflowBuilder( - _ => payloadFactory(), - resultSelector); + return RailwayBuilderFactory.Create(payloadFactory, resultSelector); } /// @@ -35,17 +35,13 @@ public static WorkflowBuilder CreateA factory function that creates the initial payload /// A function that creates the success result from the final payload /// An action that configures the workflow - /// A workflow instance - public static Workflow CreateWorkflow( + /// A railway instance + [Obsolete("Use RailwayBuilderFactory.CreateRailway instead. This method will be removed in a future version.")] + public static Railway CreateWorkflow( Func payloadFactory, Func resultSelector, - Action> configure) + Action> configure) { - var builder = new WorkflowBuilder( - _ => payloadFactory(), - resultSelector); - - configure(builder); - return builder.Build(); + return RailwayBuilderFactory.CreateRailway(payloadFactory, resultSelector, configure); } -} \ No newline at end of file +}