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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ 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).

## [2.1.0] - 2025-04-21

### Added

- Added support for branches with isolated local payloads
- New `BranchWithLocalPayload` method that allows branches to use their own payload type
- Activities within these branches can access and modify both the main payload and the local payload
- Local payloads are isolated to their branch and don't affect other parts of the workflow

## [2.0.0] - 2025-04-19

### Added
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>2.0.0</Version>
<Version>2.1.0</Version>

<!-- Source linking -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
Expand Down
100 changes: 36 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Zooper.Bee is a fluent, lightweight workflow framework for C# that enables you t
- **Functional Style**: Uses an Either monad pattern for clear success/failure handling
- **Composable**: Build complex workflows from simple, reusable components
- **Comprehensive**: Support for validations, conditional activities, branches, and finally blocks
- **Isolated Branches**: Create branches with their own isolated local payload types
- **Async-first**: First-class support for async/await operations
- **Testable**: Workflows built with Zooper.Bee are easy to unit test
- **No Dependencies**: Minimal external dependencies (only uses Zooper.Fox)
Expand Down Expand Up @@ -97,6 +98,7 @@ var workflow = new WorkflowBuilder<TRequest, TPayload, TSuccess, TError>(
.Do(...) // Add activities
.DoIf(...) // Add conditional activities
.Branch(...) // Add branching logic
.BranchWithLocalPayload(...) // Add branch with its own isolated payload type
.Finally(...) // Add finally activities
.Build(); // Build the workflow
```
Expand Down Expand Up @@ -158,77 +160,47 @@ Create branches for more complex conditional logic:
.EndBranch()
```

### Finally Blocks
### Branches with Local Payload

Activities that execute regardless of workflow success or failure:
Create isolated branches with their own local payload type that doesn't affect the main workflow payload:

```csharp
.Finally(LogOrderProcessing)
.BranchWithLocalPayload(
// Condition
payload => payload.NeedsCustomization,

// Local payload factory
mainPayload => new CustomizationPayload(
AvailableOptions: new[] { "Engraving", "Gift Wrap" },
SelectedOptions: new string[0],
CustomizationCost: 0m
),

// Branch configuration
branch => branch
.Do((mainPayload, localPayload) => {
// Activity can access and modify both payloads
var selectedOption = "Engraving";

var updatedLocalPayload = localPayload with {
SelectedOptions = new[] { selectedOption },
CustomizationCost = 10.00m
};

var updatedMainPayload = mainPayload with {
FinalPrice = mainPayload.Price + updatedLocalPayload.CustomizationCost
};

return Either<OrderError, (OrderPayload, CustomizationPayload)>.FromRight(
(updatedMainPayload, updatedLocalPayload));
})
)
```

## Advanced Usage

### Asynchronous Operations

Zooper.Bee has first-class support for async operations:
### Finally Blocks

```csharp
private static async Task<Either<OrderError, OrderPayload>> ProcessPaymentAsync(
OrderPayload payload, CancellationToken cancellationToken)
{
var result = await paymentService.AuthorizePaymentAsync(
payload.Request.Amount,
cancellationToken);
Activities that execute regardless of workflow success or failure:

if (result.Success)
{
var updatedPayload = payload with { IsProcessed = true };
return Either<OrderError, OrderPayload>.FromRight(updatedPayload);
}
else
{
return Either<OrderError, OrderPayload>.FromLeft(
new OrderError("PAYMENT_FAILED", result.ErrorMessage));
}
}
```

### Composition

Workflows can be composed by calling one workflow from another:

```csharp
private static async Task<Either<OrderError, OrderPayload>> RunSubWorkflow(
OrderPayload payload, CancellationToken cancellationToken)
{
var subWorkflow = CreateSubWorkflow();
var result = await subWorkflow.Execute(payload.SubRequest, cancellationToken);

return result.Match(
error => Either<OrderError, OrderPayload>.FromLeft(error),
success => Either<OrderError, OrderPayload>.FromRight(
payload with { SubResult = success })
);
}
```

## Examples

Check out the [Zooper.Bee.Examples](./Zooper.Bee.Examples) project for comprehensive examples including:

- Order processing workflow
- Different execution patterns
- Complex branching logic
- Error handling scenarios

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Related Projects

- [Zooper.Fox](https://github.com/zooper/fox) - Functional programming primitives used by Zooper.Bee
193 changes: 193 additions & 0 deletions Zooper.Bee.Example/BranchWithLocalPayloadExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Threading.Tasks;
using Zooper.Bee;
using Zooper.Fox;

namespace Zooper.Bee.Example;

public class BranchWithLocalPayloadExample
{
// Request models
public record OrderRequest(
int OrderId,
string CustomerName,
decimal OrderAmount,
bool NeedsShipping);

// Success model
public record OrderConfirmation(
int OrderId,
string CustomerName,
decimal TotalAmount,
string? ShippingTrackingNumber);

// Error model
public record OrderError(string Code, string Message);

// Main workflow payload model
public record OrderPayload(
int OrderId,
string CustomerName,
decimal OrderAmount,
bool NeedsShipping,
decimal TotalAmount = 0,
string? ShippingTrackingNumber = null);

// Local payload for shipping branch
public record ShippingPayload(
string CustomerAddress,
decimal ShippingCost,
decimal PackagingCost,
decimal InsuranceCost,
string? TrackingNumber = null);

public static async Task RunExample()
{
Console.WriteLine("\n=== Workflow Branch With Local Payload Example ===\n");

// Create sample requests
var standardOrder = new OrderRequest(2001, "Alice Johnson", 75.00m, false);
var shippingOrder = new OrderRequest(2002, "Bob Smith", 120.00m, true);

// Build the order processing workflow
var workflow = CreateOrderWorkflow();

// Process the standard order (no shipping)
Console.WriteLine("Processing standard order (no shipping):");
await ProcessOrder(workflow, standardOrder);

Console.WriteLine();

// Process the order with shipping
Console.WriteLine("Processing order with shipping:");
await ProcessOrder(workflow, shippingOrder);
}

private static async Task ProcessOrder(
Workflow<OrderRequest, OrderConfirmation, OrderError> workflow,
OrderRequest request)
{
var result = await workflow.Execute(request);

if (result.IsRight)
{
var confirmation = result.Right;
Console.WriteLine($"Order {confirmation.OrderId} processed successfully");
Console.WriteLine($"Customer: {confirmation.CustomerName}");
Console.WriteLine($"Total Amount: ${confirmation.TotalAmount}");

if (confirmation.ShippingTrackingNumber != null)
{
Console.WriteLine($"Shipping Tracking Number: {confirmation.ShippingTrackingNumber}");
}
else
{
Console.WriteLine("No shipping required");
}
}
else
{
var error = result.Left;
Console.WriteLine($"Order processing failed: [{error.Code}] {error.Message}");
}
}

private static Workflow<OrderRequest, OrderConfirmation, OrderError> CreateOrderWorkflow()
{
return new WorkflowBuilder<OrderRequest, OrderPayload, OrderConfirmation, OrderError>(
// Create initial payload from request
request => new OrderPayload(
request.OrderId,
request.CustomerName,
request.OrderAmount,
request.NeedsShipping),

// Create result from final payload
payload => new OrderConfirmation(
payload.OrderId,
payload.CustomerName,
payload.TotalAmount,
payload.ShippingTrackingNumber)
)
// Validate order amount
.Validate(request =>
{
if (request.OrderAmount <= 0)
{
return Option<OrderError>.Some(
new OrderError("INVALID_AMOUNT", "Order amount must be greater than zero"));
}

return Option<OrderError>.None();
})
// Process the basic order details
.Do(payload =>
{
Console.WriteLine($"Processing order {payload.OrderId} for {payload.CustomerName}...");

// Set the initial total amount to the order amount
return Either<OrderError, OrderPayload>.FromRight(
payload with { TotalAmount = payload.OrderAmount });
})
// Branch with local payload for shipping-specific processing
.BranchWithLocalPayload(
// Only enter this branch if shipping is needed
payload => payload.NeedsShipping,

// Create the local shipping payload
payload => new ShippingPayload(
CustomerAddress: "123 Example St, City, Country", // In real world, this would come from a database
ShippingCost: 12.50m,
PackagingCost: 2.75m,
InsuranceCost: 5.00m),

// Configure the branch with shipping-specific activities
branch => branch
// First shipping activity - calculate shipping costs
.Do((mainPayload, shippingPayload) =>
{
Console.WriteLine("Calculating shipping costs...");

// Calculate the total shipping cost
decimal totalShippingCost =
shippingPayload.ShippingCost +
shippingPayload.PackagingCost +
shippingPayload.InsuranceCost;

// Update both payloads
var updatedMainPayload = mainPayload with
{
TotalAmount = mainPayload.OrderAmount + totalShippingCost
};

return Either<OrderError, (OrderPayload, ShippingPayload)>.FromRight(
(updatedMainPayload, shippingPayload));
})
// Second shipping activity - generate tracking number
.Do((mainPayload, shippingPayload) =>
{
Console.WriteLine("Generating shipping tracking number...");

// Generate a fake tracking number
string trackingNumber = $"TRACK-{Guid.NewGuid().ToString()[..8]}";

// Update both payloads with the tracking number
var updatedShippingPayload = shippingPayload with { TrackingNumber = trackingNumber };
var updatedMainPayload = mainPayload with { ShippingTrackingNumber = trackingNumber };

return Either<OrderError, (OrderPayload, ShippingPayload)>.FromRight(
(updatedMainPayload, updatedShippingPayload));
})
)
// Finalize the order
.Do(payload =>
{
Console.WriteLine($"Finalizing order {payload.OrderId}...");

// In a real system, we would persist the order to a database here

return Either<OrderError, OrderPayload>.FromRight(payload);
})
.Build();
}
}
3 changes: 3 additions & 0 deletions Zooper.Bee.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public static async Task Main()

// Run the branching example
await BranchingExample.RunExample();

// Run the branch with local payload example
await BranchWithLocalPayloadExample.RunExample();
}

private static async Task ProcessOrder(OrderRequest request)
Expand Down
Loading