Result pattern integration for Fox.ChainKit. Enables seamless use of Fox.ResultKit with chain handlers, providing automatic conversion between Result and HandlerResult types with full diagnostics support.
$ dotnet add package Fox.ChainKit.ResultKitResult pattern integration for Fox.ChainKit, providing automatic failure handling and seamless Result-based chain execution.
Result objects from Fox.ResultKitResult.Failure automatically stops the chainResult.Success continues to the next handlerdotnet add package Fox.ChainKit.ResultKit
This package requires:
Fox.ChainKit (core package)Fox.ResultKit (Result pattern implementation)public class ValidationHandler : IResultHandler<OrderContext>
{
public Task<Result> HandleAsync(OrderContext context, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(context.OrderId))
{
return Task.FromResult(Result.Failure("Order ID is required"));
}
return Task.FromResult(Result.Success());
}
}
public class ProcessingHandler : IResultHandler<OrderContext>
{
public async Task<Result> HandleAsync(OrderContext context, CancellationToken cancellationToken = default)
{
try
{
await ProcessOrderAsync(context, cancellationToken);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Processing failed: {ex.Message}");
}
}
}
using Fox.ChainKit.ResultKit.Extensions;
var services = new ServiceCollection();
services.AddTransient<ValidationHandler>();
services.AddTransient<ProcessingHandler>();
var serviceProvider = services.BuildServiceProvider();
var chain = new ChainBuilder<OrderContext>(serviceProvider)
.AddResultHandler<OrderContext, ValidationHandler>()
.AddResultHandler<OrderContext, ProcessingHandler>()
.Build();
var context = new OrderContext { OrderId = "ORD-123", Amount = 150.00m };
await chain.RunAsync(context);
When a Result handler returns Result.Success():
HandlerResult.Continuepublic Task<Result> HandleAsync(OrderContext context, CancellationToken cancellationToken)
{
// Validation passes
return Task.FromResult(Result.Success());
}
When a Result handler returns Result.Failure():
HandlerResult.Stoppublic Task<Result> HandleAsync(OrderContext context, CancellationToken cancellationToken)
{
// Validation fails
return Task.FromResult(Result.Failure("Invalid order data"));
}
Capture Result objects for logging, diagnostics, or error handling:
var chain = new ChainBuilder<OrderContext>(serviceProvider)
.AddResultHandler<OrderContext, ValidationHandler>(result =>
{
if (!result.IsSuccess)
{
Console.WriteLine($"Validation failed: {result.Error}");
}
})
.AddResultHandler<OrderContext, ProcessingHandler>(result =>
{
if (!result.IsSuccess)
{
Console.WriteLine($"Processing failed: {result.Error}");
}
})
.Build();
Execute Result handlers only when conditions are met:
var chain = new ChainBuilder<OrderContext>(serviceProvider)
.AddResultHandler<OrderContext, ValidationHandler>()
.AddConditionalResultHandler<OrderContext, PremiumValidationHandler>(
ctx => ctx.Amount > 1000)
.AddResultHandler<OrderContext, ProcessingHandler>()
.Build();
Monitor Result-based chain execution:
using Fox.ChainKit.ResultKit.Extensions;
var chain = new ChainBuilder<OrderContext>(serviceProvider)
.AddResultHandler<OrderContext, ValidationHandler>()
.AddResultHandler<OrderContext, ProcessingHandler>()
.UseDiagnostics(diagnostics =>
{
Console.WriteLine(diagnostics.FormatResultDiagnostics());
// Output: "Result handlers: 2, Failed: 0, Skipped: 0"
if (diagnostics.StoppedEarly)
{
Console.WriteLine($"Chain stopped: {diagnostics.EarlyStopReason}");
}
})
.Build();
You can mix standard handlers and Result handlers in the same chain:
var chain = new ChainBuilder<OrderContext>(serviceProvider)
.AddHandler<LoggingHandler>() // Standard handler
.AddResultHandler<OrderContext, ValidationHandler>() // Result handler
.AddResultHandler<OrderContext, ProcessingHandler>() // Result handler
.AddHandler<NotificationHandler>() // Standard handler
.Build();
When mixing standard and Result handlers, you need to explicitly capture Result objects to access error information. There are three recommended approaches:
Use callbacks to capture each Result handler's output:
var results = new List<(string HandlerName, Result Result)>();
var chain = new ChainBuilder<OrderContext>(serviceProvider)
.AddHandler<LoggingHandler>()
.AddResultHandler<OrderContext, ValidationHandler>(result =>
{
results.Add(("ValidationHandler", result));
if (!result.IsSuccess)
{
logger.LogError($"Validation failed: {result.Error}");
}
})
.AddResultHandler<OrderContext, ProcessingHandler>(result =>
{
results.Add(("ProcessingHandler", result));
})
.AddHandler<NotificationHandler>()
.Build();
await chain.RunAsync(context);
// Access all captured Results
foreach (var (name, result) in results)
{
Console.WriteLine($"{name}: {(result.IsSuccess ? "Success" : $"Failed - {result.Error}")}");
}
Add a Result collection to your context:
public class OrderContext
{
public string OrderId { get; set; }
public decimal Amount { get; set; }
// Result storage
public List<Result> Results { get; } = [];
}
// In your Result handler:
public Task<Result> HandleAsync(OrderContext context, CancellationToken cancellationToken)
{
var result = PerformValidation(context);
context.Results.Add(result); // Store in context
return Task.FromResult(result);
}
// After chain execution:
await chain.RunAsync(context);
if (context.Results.Any(r => !r.IsSuccess))
{
var errors = context.Results.Where(r => !r.IsSuccess).Select(r => r.Error);
Console.WriteLine($"Chain failed with errors: {string.Join(", ", errors)}");
}
Create a collector service for cross-cutting Result tracking:
public sealed class ResultCollector
{
public List<(string HandlerName, Result Result)> Results { get; } = [];
public void Collect(string handlerName, Result result)
{
Results.Add((handlerName, result));
}
public bool HasFailures => Results.Any(r => !r.Result.IsSuccess);
public IEnumerable<string> GetFailureMessages() =>
Results.Where(r => !r.Result.IsSuccess).Select(r => r.Result.Error ?? "Unknown error");
}
// Register as scoped or singleton
services.AddScoped<ResultCollector>();
// Inject into context or handlers
public class OrderContext
{
public ResultCollector ResultCollector { get; set; } = null!;
}
// Use in Result handler
public Task<Result> HandleAsync(OrderContext context, CancellationToken cancellationToken)
{
var result = PerformValidation(context);
context.ResultCollector.Collect("ValidationHandler", result);
return Task.FromResult(result);
}
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Callbacks | Simple scenarios | No context modification needed | Verbose for many handlers |
| Context Storage | Business logic needs Results | Results available in context | Couples context to Result pattern |
| Collector Service | Cross-cutting concerns | Clean separation, reusable | Additional service registration |
Recommendation: Use callbacks for simple cases, context storage when business logic needs Results, and collector service for complex audit/logging scenarios.
Result.Failure() for expected failuresTest Result handlers independently:
[Fact]
public async Task ValidationHandler_should_return_failure_for_empty_order_id()
{
var handler = new ValidationHandler();
var context = new OrderContext { OrderId = "" };
var result = await handler.HandleAsync(context);
result.IsSuccess.Should().BeFalse();
result.Error.Should().Be("Order ID is required");
}
[Fact]
public async Task ValidationHandler_should_return_success_for_valid_order()
{
var handler = new ValidationHandler();
var context = new OrderContext { OrderId = "ORD-123", Amount = 100m };
var result = await handler.HandleAsync(context);
result.IsSuccess.Should().BeTrue();
}
Use IResultHandler<TContext> when:
Use IHandler<TContext> when:
| Feature | IHandler | IResultHandler |
|---|---|---|
| Return Type | HandlerResult | Result |
| Success | HandlerResult.Continue | Result.Success() |
| Failure | HandlerResult.Stop | Result.Failure(error) |
| Error Info | Not included | Included in Result |
| Use Case | General purpose | Business logic with failures |
MIT License - see LICENSE file for details.
Contributions are welcome! Please open an issue or submit a pull request.