A flexible and powerful railway-oriented programming library for .NET that allows you to define complex business processes with a fluent API.
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.
- 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
dotnet add package Zooper.Bee// Define a simple railway
var railway = new RailwayBuilder<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 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
}
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 railway begins execution. They're ideal for authentication, authorization, account validation, or any other requirement that must be satisfied before a railway 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 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 railway, preventing unnecessary work
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.
// 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 railway.
.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 railway.
.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 railway 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 railway.
.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 railway components with a single extension method:
// In Startup.cs or Program.cs
services.AddRailways();This will scan all assemblies and register:
- All railway validations
- All railway activities
- All concrete railway classes (classes ending with "Railway")
You can also register specific components:
// Register only validations
services.AddRailwayValidations();
// Register only activities
services.AddRailwayActivities();
// Specify which assemblies to scan
services.AddRailways(new[] { typeof(Program).Assembly });
// Specify service lifetime (Singleton, Scoped, Transient)
services.AddRailways(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 railway - 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 railway 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
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.
| Old Name | New Name |
|---|---|
Workflow<TRequest, TSuccess, TError> |
Railway<TRequest, TSuccess, TError> |
WorkflowBuilder<...> |
RailwayBuilder<...> |
WorkflowBuilderFactory |
RailwayBuilderFactory |
CreateWorkflow<...>() |
CreateRailway<...>() |
IWorkflowStep |
IRailwayStep |
IWorkflowValidation |
IRailwayValidation |
IWorkflowGuard |
IRailwayGuard |
AddWorkflows() |
AddRailways() |
AddWorkflowSteps() |
AddRailwaySteps() |
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.
- Replace all
Workflow<type references withRailway< - Replace
WorkflowBuilder<withRailwayBuilder< - Replace
WorkflowBuilderFactory.CreateWorkflow<withRailwayBuilderFactory.CreateRailway< - Replace DI registration calls (
AddWorkflows()->AddRailways(), etc.) - Update any interface implementations (
IWorkflowStep->IRailwayStep, etc.)
MIT License (Copyright details here)