A flexible and powerful workflow library for .NET that allows you to define complex business processes with a fluent API.
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, conditional, parallel, and detached operations.
- 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
dotnet add package Zooper.Bee// Define a simple workflow
var workflow = new WorkflowBuilder<Request, Payload, SuccessResult, ErrorResult>(
// Factory function that creates the initial payload from the request
request => new Payload { Data = request.Data },
// Selector function that creates the success result from the final payload
payload => new SuccessResult { ProcessedData = payload.Data }
)
.Validate(request =>
{
// Validate the request
if (string.IsNullOrEmpty(request.Data))
return Option<ErrorResult>.Some(new ErrorResult { Message = "Data is required" });
return Option<ErrorResult>.None;
})
.Do(payload =>
{
// Process the payload
payload.Data = payload.Data.ToUpper();
return Either<ErrorResult, Payload>.FromRight(payload);
})
.Build();
// Execute the workflow
var result = await workflow.Execute(new Request { Data = "hello world" }, CancellationToken.None);
if (result.IsRight)
{
Console.WriteLine($"Success: {result.Right.ProcessedData}"); // Output: Success: HELLO WORLD
}
else
{
Console.WriteLine($"Error: {result.Left.Message}");
}Validates the incoming request before processing begins.
// Asynchronous validation
.Validate(async (request, cancellationToken) =>
{
var isValid = await ValidateAsync(request, cancellationToken);
return isValid ? Option<ErrorResult>.None : Option<ErrorResult>.Some(new ErrorResult());
})
// Synchronous validation
.Validate(request =>
{
var isValid = Validate(request);
return isValid ? Option<ErrorResult>.None : Option<ErrorResult>.Some(new ErrorResult());
})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.
// 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());
})- 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 are the building blocks of a workflow. They process the payload and can produce either a success (with the modified payload) or an error.
// Asynchronous activity
.Do(async (payload, cancellationToken) =>
{
var result = await ProcessAsync(payload, cancellationToken);
return Either<ErrorResult, Payload>.FromRight(result);
})
// Synchronous activity
.Do(payload =>
{
var result = Process(payload);
return Either<ErrorResult, Payload>.FromRight(result);
})
// Multiple activities
.DoAll(
payload => DoFirstThing(payload),
payload => DoSecondThing(payload),
payload => DoThirdThing(payload)
)Activities that only execute if a condition is met.
.DoIf(
payload => payload.ShouldProcess, // Condition
payload =>
{
// Activity that only executes if the condition is true
payload.Data = Process(payload.Data);
return Either<ErrorResult, Payload>.FromRight(payload);
}
)Organize related activities into logical groups. Groups can have conditions and always merge their results back to the main workflow.
.Group(
payload => payload.ShouldProcessGroup, // Optional condition
group => group
.Do(payload => FirstActivity(payload))
.Do(payload => SecondActivity(payload))
.Do(payload => ThirdActivity(payload))
)Create a context with the local state that is accessible to all activities within the context. This helps encapsulate related operations.
.WithContext(
null, // No condition, always execute
payload => new LocalState { Counter = 0 }, // Create local state
context => context
.Do((payload, state) =>
{
state.Counter++;
return (payload, state);
})
.Do((payload, state) =>
{
payload.Result = $"Counted to {state.Counter}";
return (payload, state);
})
)Execute multiple groups of activities in parallel and merge the results.
.Parallel(
null, // No condition, always execute
parallel => parallel
.Group(group => group
.Do(payload => { payload.Result1 = "Result 1"; return payload; })
)
.Group(group => group
.Do(payload => { payload.Result2 = "Result 2"; return payload; })
)
)Execute activities in the background without waiting for their completion. Results from detached activities are not merged back into the main workflow.
.Detach(
null, // No condition, always execute
detached => detached
.Do(payload =>
{
// This runs in the background
LogActivity(payload);
return payload;
})
)Execute multiple groups of detached activities in parallel without waiting for completion.
.ParallelDetached(
null, // No condition, always execute
parallelDetached => parallelDetached
.Detached(detached => detached
.Do(payload => { LogActivity1(payload); return payload; })
)
.Detached(detached => detached
.Do(payload => { LogActivity2(payload); return payload; })
)
)Activities that always execute, even if the workflow fails.
.Finally(payload =>
{
// Cleanup or logging
CleanupResources(payload);
return Either<ErrorResult, Payload>.FromRight(payload);
}).Do(payload =>
{
try
{
var result = RiskyOperation(payload);
return Either<ErrorResult, Payload>.FromRight(result);
}
catch (Exception ex)
{
return Either<ErrorResult, Payload>.FromLeft(new ErrorResult { Message = ex.Message });
}
})Use conditions to determine which path to take in a workflow.
.Group(
payload => payload.Type == "TypeA",
group => group
.Do(payload => ProcessTypeA(payload))
)
.Group(
payload => payload.Type == "TypeB",
group => group
.Do(payload => ProcessTypeB(payload))
)Zooper.Bee integrates seamlessly with .NET's dependency injection system. You can register all workflow components with a single extension method:
// In Startup.cs or Program.cs
services.AddWorkflows();This will scan all assemblies and register:
- All workflow validations
- All workflow activities
- All concrete workflow classes (classes ending with "Workflow")
You can also register specific components:
// Register only validations
services.AddWorkflowValidations();
// Register only activities
services.AddWorkflowActivities();
// Specify which assemblies to scan
services.AddWorkflows(new[] { typeof(Program).Assembly });
// Specify service lifetime (Singleton, Scoped, Transient)
services.AddWorkflows(lifetime: ServiceLifetime.Singleton);- Use
Parallelfor CPU-bound operations that can benefit from parallel execution - Use
Detachfor I/O operations that don't affect the main workflow - Be mindful of resource contention in parallel operations
- Consider using
WithContextto maintain state between related activities
- Keep activities small and focused on a single responsibility
- Use descriptive names for your workflow methods
- Group related activities together
- Handle errors at appropriate levels
- Use
Finallyfor cleanup operations - Validate requests early to fail fast
- Use contextual state to avoid passing too many parameters
MIT License (Copyright details here)