diff --git a/CHANGELOG.md b/CHANGELOG.md index 7842ac2..21ef05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.3.0 - 2025.04.24 + +### Added + +- New guards feature for verifying workflow execution requirements + - Added `Guard` methods to check if a workflow can be executed + - Guards run after validations and before Activities and provide early termination + +- New component interfaces for dependency injection and workflow composition + - Added `IWorkflowGuard` and `IWorkflowGuard` interfaces + - Added `IWorkflowGuards` and `IWorkflowGuards` interfaces + +- New extension methods for registering guards with dependency injection + - Added `AddWorkflowGuards()` for registering workflow guards + ## 3.2.1 - 2025.04.24 ### Modified diff --git a/Directory.Build.props b/Directory.Build.props index ead2781..0a15210 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ A .NET library for building robust, functional workflows and processing pipelines. - 3.2.1 + 3.3.0 true diff --git a/README.md b/README.md index 4150d24..dcb96ce 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,7 @@ else ## Building Workflows -### Basic Operations - -#### Validation +### Validation Validates the incoming request before processing begins. @@ -91,7 +89,35 @@ Validates the incoming request before processing begins. }) ``` -#### Activities +### 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. + +```csharp +// Asynchronous guard +.Guard(async (request, cancellationToken) => +{ + var isAuthorized = await CheckAuthorizationAsync(request, cancellationToken); + return isAuthorized ? Option.None : Option.Some(new ErrorResult()); +}) + +// Synchronous guard +.Guard(request => +{ + var isAuthorized = CheckAuthorization(request); + return isAuthorized ? Option.None : Option.Some(new ErrorResult()); +}) +``` + +#### 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 +- Common checks like authentication can be standardized and reused +- Failures short-circuit the workflow, preventing unnecessary work + +### Activities Activities are the building blocks of a workflow. They process the payload and can produce either a success (with the modified payload) or an error. @@ -119,7 +145,7 @@ the modified payload) or an error. ) ``` -#### Conditional Activities +### Conditional Activities Activities that only execute if a condition is met. @@ -135,9 +161,7 @@ Activities that only execute if a condition is met. ) ``` -### Advanced Features - -#### Groups +### Groups Organize related activities into logical groups. Groups can have conditions and always merge their results back to the main workflow. @@ -152,7 +176,7 @@ main workflow. ) ``` -#### Contexts with Local State +### Contexts with Local State Create a context with the local state that is accessible to all activities within the context. This helps encapsulate related operations. @@ -175,7 +199,7 @@ related operations. ) ``` -#### Parallel Execution +### Parallel Execution Execute multiple groups of activities in parallel and merge the results. @@ -192,7 +216,7 @@ Execute multiple groups of activities in parallel and merge the results. ) ``` -#### Detached Execution +### Detached Execution Execute activities in the background without waiting for their completion. Results from detached activities are not merged back into the main workflow. @@ -210,7 +234,7 @@ merged back into the main workflow. ) ``` -#### Parallel Detached Execution +### Parallel Detached Execution Execute multiple groups of detached activities in parallel without waiting for completion. @@ -227,7 +251,7 @@ Execute multiple groups of detached activities in parallel without waiting for c ) ``` -#### Finally Block +### Finally Block Activities that always execute, even if the workflow fails. diff --git a/Zooper.Bee/Extensions/WorkflowExtensions.cs b/Zooper.Bee/Extensions/WorkflowExtensions.cs index 706e9da..c526b0e 100644 --- a/Zooper.Bee/Extensions/WorkflowExtensions.cs +++ b/Zooper.Bee/Extensions/WorkflowExtensions.cs @@ -76,6 +76,9 @@ public static IServiceCollection AddWorkflows( // Register all workflow validations services.AddWorkflowValidations(assembliesToScan, lifetime); + // Register all workflow guards + services.AddWorkflowGuards(assembliesToScan, lifetime); + // Register all workflow activities services.AddWorkflowActivities(assembliesToScan, lifetime); diff --git a/Zooper.Bee/Extensions/WorkflowGuardExtensions.cs b/Zooper.Bee/Extensions/WorkflowGuardExtensions.cs new file mode 100644 index 0000000..0142a2a --- /dev/null +++ b/Zooper.Bee/Extensions/WorkflowGuardExtensions.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 workflow guards with dependency injection. +/// +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 + 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; + } +} \ No newline at end of file diff --git a/Zooper.Bee/Interfaces/IWorkflowGuard.cs b/Zooper.Bee/Interfaces/IWorkflowGuard.cs new file mode 100644 index 0000000..18340e1 --- /dev/null +++ b/Zooper.Bee/Interfaces/IWorkflowGuard.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all workflow guards. +/// +public interface IWorkflowGuard; + +/// +/// 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 diff --git a/Zooper.Bee/Interfaces/IWorkflowGuards.cs b/Zooper.Bee/Interfaces/IWorkflowGuards.cs new file mode 100644 index 0000000..372a489 --- /dev/null +++ b/Zooper.Bee/Interfaces/IWorkflowGuards.cs @@ -0,0 +1,13 @@ +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all workflow guards. +/// +public interface IWorkflowGuards; + +/// +/// 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 diff --git a/Zooper.Bee/Internal/WorkflowGuard.cs b/Zooper.Bee/Internal/WorkflowGuard.cs new file mode 100644 index 0000000..a170a0e --- /dev/null +++ b/Zooper.Bee/Internal/WorkflowGuard.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Internal; + +/// +/// Represents a guard that checks if a workflow can be executed. +/// +/// Type of the request +/// Type of the error +internal sealed class WorkflowGuard +{ + private readonly Func>> _condition; + private readonly string? _name; + + public WorkflowGuard( + Func>> condition, + string? name = null) + { + _condition = condition; + _name = name; + } + + public Task> Check( + TRequest request, + CancellationToken token) + { + return _condition(request, token); + } +} \ No newline at end of file diff --git a/Zooper.Bee/WorkflowBuilder.cs b/Zooper.Bee/WorkflowBuilder.cs index 04fc8b6..0ceec49 100644 --- a/Zooper.Bee/WorkflowBuilder.cs +++ b/Zooper.Bee/WorkflowBuilder.cs @@ -29,15 +29,16 @@ public sealed class WorkflowBuilder private readonly Func _contextFactory; private readonly Func _resultSelector; - private readonly List> _validations = new(); - private readonly List> _activities = new(); - private readonly List> _conditionalActivities = new(); - private readonly List> _finallyActivities = new(); - private readonly List> _branches = new(); - private readonly List _branchesWithLocalPayload = new(); + 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 = new(); + private readonly List> _features = []; /// /// Initializes a new instance of the class. @@ -86,6 +87,36 @@ public WorkflowBuilder Validate(Func + /// 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. /// @@ -505,10 +536,15 @@ 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!)); @@ -576,6 +612,28 @@ private async Task> RunValidationsAsync( 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. ///