Skip to content

zooper-lib/Bee

Repository files navigation

Zooper.Bee

Zooper.Bee Logo

NuGet Version License: MIT

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 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

  • 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

dotnet add package Zooper.Bee

Getting Started

// 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}");
}

Building Railways

Validation

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

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());
})

Benefits of Guards

  • 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

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)
)

Conditional Activities

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);
    }
)

Groups

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))
)

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.

.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);
        })
)

Parallel Execution

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; })
        )
)

Detached Execution

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;
        })
)

Parallel Detached Execution

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; })
        )
)

Finally Block

Activities that always execute, even if the railway fails.

.Finally(payload =>
{
    // Cleanup or logging
    CleanupResources(payload);
    return Either<ErrorResult, Payload>.FromRight(payload);
})

Advanced Patterns

Error Handling

.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 });
    }
})

Conditional Branching

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))
)

Dependency Injection Integration

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);

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 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 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<TRequest, TSuccess, TError> Railway<TRequest, TSuccess, TError>
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)

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages