Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TRequest, TError>` interfaces
- Added `IWorkflowGuards` and `IWorkflowGuards<TRequest, TError>` interfaces

- New extension methods for registering guards with dependency injection
- Added `AddWorkflowGuards()` for registering workflow guards

## 3.2.1 - 2025.04.24

### Modified
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<Description>A .NET library for building robust, functional workflows and processing pipelines.</Description>

<!-- Version information -->
<Version>3.2.1</Version>
<Version>3.3.0</Version>

<!-- Source linking -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
Expand Down
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ else

## Building Workflows

### Basic Operations

#### Validation
### Validation

Validates the incoming request before processing begins.

Expand All @@ -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<ErrorResult>.None : Option<ErrorResult>.Some(new ErrorResult());
})

// Synchronous guard
.Guard(request =>
{
var isAuthorized = CheckAuthorization(request);
return isAuthorized ? Option<ErrorResult>.None : Option<ErrorResult>.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.
Expand Down Expand Up @@ -119,7 +145,7 @@ the modified payload) or an error.
)
```

#### Conditional Activities
### Conditional Activities

Activities that only execute if a condition is met.

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -175,7 +199,7 @@ related operations.
)
```

#### Parallel Execution
### Parallel Execution

Execute multiple groups of activities in parallel and merge the results.

Expand All @@ -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.
Expand All @@ -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.

Expand All @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions Zooper.Bee/Extensions/WorkflowExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
54 changes: 54 additions & 0 deletions Zooper.Bee/Extensions/WorkflowGuardExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides extension methods for registering workflow guards with dependency injection.
/// </summary>
public static class WorkflowGuardExtensions
{
/// <summary>
/// Registers all workflow guards from the specified assemblies into the service collection.
/// This includes both individual workflow guards (IWorkflowGuard) and guard collections (IWorkflowGuards).
/// </summary>
/// <param name="services">The service collection to add the registrations to</param>
/// <param name="assemblies">Optional list of assemblies to scan for workflow guards. If null or empty, all non-system
/// assemblies in the current AppDomain will be scanned</param>
/// <param name="lifetime">The service lifetime to use for the registered services (defaults to Scoped)</param>
/// <returns>The service collection for chaining additional registrations</returns>
public static IServiceCollection AddWorkflowGuards(
this IServiceCollection services,
IEnumerable<Assembly>? 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;
}
}
28 changes: 28 additions & 0 deletions Zooper.Bee/Interfaces/IWorkflowGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
using Zooper.Fox;

namespace Zooper.Bee.Interfaces;

/// <summary>
/// Base marker interface for all workflow guards.
/// </summary>
public interface IWorkflowGuard;

/// <summary>
/// Represents a guard that checks if a workflow can be executed.
/// </summary>
/// <typeparam name="TRequest">The type of the request.</typeparam>
/// <typeparam name="TError">The type of the error.</typeparam>
public interface IWorkflowGuard<in TRequest, TError> : IWorkflowGuard
{
/// <summary>
/// Checks if the workflow can be executed with the given request.
/// </summary>
/// <param name="request">The workflow request.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>Either an error if the guard fails, or Unit if it succeeds.</returns>
Task<Either<TError, Unit>> ExecuteAsync(
TRequest request,
CancellationToken cancellationToken);
}
13 changes: 13 additions & 0 deletions Zooper.Bee/Interfaces/IWorkflowGuards.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Zooper.Bee.Interfaces;

/// <summary>
/// Base marker interface for all workflow guards.
/// </summary>
public interface IWorkflowGuards;

/// <summary>
/// Interface for a collection of workflow guards operating on the same request and error types.
/// </summary>
/// <typeparam name="TRequest">The type of the request.</typeparam>
/// <typeparam name="TError">The type of the error.</typeparam>
public interface IWorkflowGuards<TRequest, TError> : IWorkflowGuards;
32 changes: 32 additions & 0 deletions Zooper.Bee/Internal/WorkflowGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Zooper.Fox;

namespace Zooper.Bee.Internal;

/// <summary>
/// Represents a guard that checks if a workflow can be executed.
/// </summary>
/// <typeparam name="TRequest">Type of the request</typeparam>
/// <typeparam name="TError">Type of the error</typeparam>
internal sealed class WorkflowGuard<TRequest, TError>
{
private readonly Func<TRequest, CancellationToken, Task<Either<TError, Unit>>> _condition;
private readonly string? _name;

public WorkflowGuard(
Func<TRequest, CancellationToken, Task<Either<TError, Unit>>> condition,
string? name = null)
{
_condition = condition;
_name = name;
}

public Task<Either<TError, Unit>> Check(
TRequest request,
CancellationToken token)
{
return _condition(request, token);
}
}
72 changes: 65 additions & 7 deletions Zooper.Bee/WorkflowBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ public sealed class WorkflowBuilder<TRequest, TPayload, TSuccess, TError>
private readonly Func<TRequest, TPayload> _contextFactory;
private readonly Func<TPayload, TSuccess> _resultSelector;

private readonly List<WorkflowValidation<TRequest, TError>> _validations = new();
private readonly List<WorkflowActivity<TPayload, TError>> _activities = new();
private readonly List<ConditionalWorkflowActivity<TPayload, TError>> _conditionalActivities = new();
private readonly List<WorkflowActivity<TPayload, TError>> _finallyActivities = new();
private readonly List<Branch<TPayload, TError>> _branches = new();
private readonly List<object> _branchesWithLocalPayload = new();
private readonly List<WorkflowGuard<TRequest, TError>> _guards = [];
private readonly List<WorkflowValidation<TRequest, TError>> _validations = [];
private readonly List<WorkflowActivity<TPayload, TError>> _activities = [];
private readonly List<ConditionalWorkflowActivity<TPayload, TError>> _conditionalActivities = [];
private readonly List<WorkflowActivity<TPayload, TError>> _finallyActivities = [];
private readonly List<Branch<TPayload, TError>> _branches = [];
private readonly List<object> _branchesWithLocalPayload = [];

// Collections for new features
private readonly List<IWorkflowFeature<TPayload, TError>> _features = new();
private readonly List<IWorkflowFeature<TPayload, TError>> _features = [];

/// <summary>
/// Initializes a new instance of the <see cref="WorkflowBuilder{TRequest, TPayload, TSuccess, TError}"/> class.
Expand Down Expand Up @@ -86,6 +87,36 @@ public WorkflowBuilder<TRequest, TPayload, TSuccess, TError> Validate(Func<TRequ
return this;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="guard">The guard function that returns Either an error or Unit</param>
/// <returns>The builder instance for method chaining</returns>
public WorkflowBuilder<TRequest, TPayload, TSuccess, TError> Guard(
Func<TRequest, CancellationToken, Task<Either<TError, Unit>>> guard)
{
_guards.Add(new(guard));
return this;
}

/// <summary>
/// Adds a synchronous guard to check if the workflow can be executed.
/// </summary>
/// <param name="guard">The guard function that returns Either an error or Unit</param>
/// <returns>The builder instance for method chaining</returns>
public WorkflowBuilder<TRequest, TPayload, TSuccess, TError> Guard(Func<TRequest, Either<TError, Unit>> guard)
{
_guards.Add(
new((
request,
_) => Task.FromResult(guard(request))
)
);
return this;
}

/// <summary>
/// Adds an activity to the workflow.
/// </summary>
Expand Down Expand Up @@ -505,10 +536,15 @@ private async Task<Either<TError, TSuccess>> 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<TError, TSuccess>.FromLeft(validationResult.Left!);

var guardResult = await RunGuardsAsync(request, cancellationToken);
if (guardResult.IsLeft)
return Either<TError, TSuccess>.FromLeft(guardResult.Left!);

var payload = _contextFactory(request);
if (payload == null)
return Either<TError, TSuccess>.FromRight(_resultSelector(default!));
Expand Down Expand Up @@ -576,6 +612,28 @@ private async Task<Either<TError, TPayload>> RunValidationsAsync(
return Either<TError, TPayload>.FromRight(default!);
}

/// <summary>
/// Runs all configured guards against the request.
/// </summary>
/// <param name="request">The workflow request.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>
/// An Either with Left if any guard fails, or Right with Unit on success.
/// </returns>
private async Task<Either<TError, Unit>> 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<TError, Unit>.FromLeft(result.Left);
}

return Either<TError, Unit>.FromRight(Unit.Value);
}

/// <summary>
/// Executes all registered activities in sequence, returning either the first encountered error or the transformed payload.
/// </summary>
Expand Down