CleanResult.WolverineFx
1.4.1
dotnet add package CleanResult.WolverineFx --version 1.4.1
NuGet\Install-Package CleanResult.WolverineFx -Version 1.4.1
<PackageReference Include="CleanResult.WolverineFx" Version="1.4.1" />
<PackageVersion Include="CleanResult.WolverineFx" Version="1.4.1" />
<PackageReference Include="CleanResult.WolverineFx" />
paket add CleanResult.WolverineFx --version 1.4.1
#r "nuget: CleanResult.WolverineFx, 1.4.1"
#:package CleanResult.WolverineFx@1.4.1
#addin nuget:?package=CleanResult.WolverineFx&version=1.4.1
#tool nuget:?package=CleanResult.WolverineFx&version=1.4.1
CleanResult.WolverineFx

<div align="center">
WolverineFx messaging integration for CleanResult
Automatic Result handling in WolverineFx message handlers with compile-time code generation
Main Documentation • Features • Usage • How It Works
</div>
📦 Installation
dotnet add package CleanResult.WolverineFx
Requirements:
- .NET 8.0 or 9.0
- WolverineFx 3.0+
Note: .NET 10.0 support is pending WolverineFx compatibility updates.
✨ Features
- 🚀 Automatic Error Handling - Short-circuits on errors without boilerplate
- 🎯 Value Extraction - Extracts success values from
Result<T>automatically - ⚡ Zero Runtime Overhead - Uses compile-time code generation (no reflection)
- 🔄 Type Conversion - Automatically converts errors between Result types
- 📦 Tuple Support - Handles tuple return values from Result<(T1, T2)>
- 🔍 Query Helpers - Special helpers for query patterns (null checks)
🚀 Usage
Registration
Register the continuation strategy in your WolverineFx configuration:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine(opts =>
{
// ✅ Add CleanResult continuation strategy
opts.CodeGeneration.AddContinuationStrategy<CleanResultContinuationStrategy>();
});
var app = builder.Build();
app.Run();
Basic Handler Pattern
WolverineFx handlers can use Load/LoadAsync methods that return Result<T>. The framework automatically:
- Calls Load/LoadAsync
- Checks for errors
- Extracts the success value
- Passes it to Handle method
public class CreateUserHandler
{
// ✅ Load returns Result<T> - validated before Handle is called
public static async Task<Result<User>> LoadAsync(CreateUserCommand command)
{
// Validation logic
if (string.IsNullOrEmpty(command.Email))
return Result<User>.Error("Email is required", 400);
var existing = await _repository.FindByEmailAsync(command.Email);
if (existing != null)
return Result<User>.Error("Email already exists", 409);
// Return new user to be created
return Result.Ok(new User { Email = command.Email, Name = command.Name });
}
// ✅ Handle receives the extracted User only if Load succeeded
public static async Task<Result<UserCreatedEvent>> Handle(
CreateUserCommand command,
User user) // This is the extracted value from LoadAsync!
{
await _repository.SaveAsync(user);
return Result.Ok(new UserCreatedEvent(user.Id, user.Email));
}
}
Generated Code
The framework generates code similar to this:
var loadResult = await CreateUserHandler.LoadAsync(command);
// Automatic error check
if (loadResult.IsError())
{
await context.EnqueueCascadingAsync(
Result<UserCreatedEvent>.Error(loadResult.ErrorValue)
);
return;
}
// Extract value and pass to Handle
var user = loadResult.Value;
var handleResult = await CreateUserHandler.Handle(command, user);
await context.EnqueueCascadingAsync(handleResult);
Tuple Support
public class OrderHandler
{
// ✅ Return multiple values as tuple
public static async Task<Result<(User, Product)>> LoadAsync(CreateOrderCommand command)
{
var user = await _userRepo.FindAsync(command.UserId);
if (user == null)
return Result.Error<(User, Product)>("User not found", 404);
var product = await _productRepo.FindAsync(command.ProductId);
if (product == null)
return Result.Error<(User, Product)>("Product not found", 404);
return Result.Ok((user, product));
}
// ✅ Handle receives both values automatically
public static async Task<Result<Order>> Handle(
CreateOrderCommand command,
User user, // Extracted from tuple
Product product) // Extracted from tuple
{
if (product.Stock < command.Quantity)
return Result<Order>.Error("Insufficient stock", 409);
var order = new Order
{
UserId = user.Id,
ProductId = product.Id,
Quantity = command.Quantity
};
await _orderRepo.SaveAsync(order);
return Result.Ok(order);
}
}
Query Helpers
Special helpers for query patterns where null indicates "not found":
public class GetUserHandler
{
public static async Task<Result<User>> LoadAsync(GetUserQuery query)
{
var user = await _repository.FindByIdAsync(query.UserId);
// ✅ Check if error OR null
if (user.IsQueryError())
return Result<User>.Error("User not found", 404);
return Result.Ok(user);
}
// Alternative: automatically convert null to 404
public static async Task<Result<User>> LoadAsync(GetUserQuery query)
{
var user = await _repository.FindByIdAsync(query.UserId);
// ✅ Converts null to 404 error automatically
return user.ToQueryError("User not found");
}
}
🔧 How It Works
Continuation Strategy
CleanResult.WolverineFx implements a custom IContinuationStrategy that hooks into WolverineFx's code generation
pipeline:
- Detection Phase - Scans for Load/LoadAsync methods returning
ResultorResult<T> - Frame Creation - Creates continuation frames that inject error-checking code
- Code Generation - Generates C# code that:
- Calls the Load method
- Checks
IsError() - Short-circuits on error with proper type conversion
- Extracts success values
- Passes values to Handle method
Architecture
Message arrives → LoadAsync called
↓
Returns Result<T>
↓
[Generated Code]
IsError() check
↓
┌─────────────┴─────────────┐
↓ Error ↓ Success
Convert error type Extract value
Return immediately Pass to Handle
↓
Handle executes
↓
Return Result
💡 Examples
Complete Handler
public record UpdateProductCommand(Guid ProductId, string Name, decimal Price, Guid CategoryId);
public class UpdateProductHandler
{
private readonly IProductRepository _products;
private readonly ICategoryRepository _categories;
public async Task<Result<(Product, Category)>> LoadAsync(
UpdateProductCommand command,
IQuerySession session)
{
// Load and validate product
var product = await session.LoadAsync<Product>(command.ProductId);
if (product == null)
return Result.Error<(Product, Category)>("Product not found", 404);
// Load and validate category
var category = await session.LoadAsync<Category>(command.CategoryId);
if (category == null)
return Result.Error<(Product, Category)>("Category not found", 404);
return Result.Ok((product, category));
}
public async Task<Result<ProductDto>> Handle(
UpdateProductCommand command,
Product product,
Category category,
IDocumentSession session)
{
// Update product
product.Name = command.Name;
product.Price = command.Price;
product.CategoryId = category.Id;
session.Update(product);
await session.SaveChangesAsync();
return Result.Ok(new ProductDto(product));
}
}
Error Handling Patterns
public class PlaceOrderHandler
{
public static async Task<Result<OrderContext>> LoadAsync(
PlaceOrderCommand command,
IQuerySession session)
{
// Multiple validation steps
var user = await session.LoadAsync<User>(command.UserId);
if (user == null)
return Result.Error("Invalid user", 400);
var product = await session.LoadAsync<Product>(command.ProductId);
if (product == null)
return Result.Error("Invalid product", 400);
if (product.Stock < command.Quantity)
return Result.Error("Insufficient stock", 409);
// Return context with validated data
return Result.Ok(new OrderContext(user, product, command.Quantity));
}
public static async Task<Result<OrderConfirmation>> Handle(
PlaceOrderCommand command,
OrderContext context, // This is the extracted value from LoadAsync
IDocumentSession session)
{
// Business logic with validated data
var order = new Order
{
UserId = context.User.Id,
ProductId = context.Product.Id,
Quantity = context.Quantity
};
session.Store(order);
// Update stock
context.Product.Stock -= context.Quantity;
session.Update(context.Product);
await session.SaveChangesAsync();
return Result.Ok(new OrderConfirmation(order.Id));
}
}
🎯 Best Practices
✅ Do's
// ✅ Use Load for validation and data loading
public static async Task<Result<Data>> LoadAsync(Command cmd, IQuerySession session)
{
var data = await session.LoadAsync<Data>(cmd.Id);
if (data == null)
return Result<Data>.Error("Not found", 404);
return Result.Ok(data);
}
// ✅ Use Handle for business logic with validated data
public static async Task<Result<Response>> Handle(
Command cmd,
Data data,
IDocumentSession session)
{
// Business logic here
session.Update(data);
await session.SaveChangesAsync();
return Result.Ok(new Response());
}
// ✅ Return tuples for multiple dependencies
public static async Task<Result<(User, Order)>> LoadAsync(
Command cmd,
IQuerySession session)
{
var user = await session.LoadAsync<User>(cmd.UserId);
var order = await session.LoadAsync<Order>(cmd.OrderId);
return Result.Ok((user, order));
}
❌ Don'ts
// ❌ Don't put business logic in Load
public static async Task<Result<User>> LoadAsync(Command cmd, IDocumentSession session)
{
var user = await session.LoadAsync<User>(cmd.Id);
session.Update(user); // Bad: side effects in Load
return Result.Ok(user);
}
// ❌ Don't skip validation in Load
public static async Task<Result<User>> LoadAsync(Command cmd, IQuerySession session)
{
var user = await session.LoadAsync<User>(cmd.Id);
return Result.Ok(user); // Bad: didn't check if null
}
// ❌ Don't access Value without checking in custom code
var result = await LoadAsync(cmd, session);
var value = result.Value; // Bad: might throw
// Let the framework handle value extraction
🔗 Related Packages
- CleanResult - Core Result implementation
- CleanResult.FluentValidation - FluentValidation integration
- CleanResult.Swashbuckle - Swagger/OpenAPI integration
- CleanResult.AspNet - IActionResult adapter
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
<div align="center">
⬆ Back to Top • Main Documentation
</div>
<div align="center"> Gwynbleid85 © 2025 </div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- CleanResult (>= 1.4.1)
- WolverineFx (>= 5.31.1)
-
net9.0
- CleanResult (>= 1.4.1)
- WolverineFx (>= 5.31.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.4.1 | 157 | 4/17/2026 |
| 1.4.0 | 97 | 4/16/2026 |
| 1.3.4 | 185 | 1/5/2026 |
| 1.3.3 | 230 | 11/25/2025 |
| 1.3.2 | 1,624 | 11/25/2025 |
| 1.3.1 | 208 | 11/25/2025 |
| 1.3.0 | 480 | 11/4/2025 |
| 1.2.9 | 207 | 10/9/2025 |
| 1.2.8 | 405 | 10/8/2025 |
| 1.2.7 | 492 | 8/25/2025 |
| 1.2.6 | 234 | 8/11/2025 |
| 1.2.5 | 438 | 7/28/2025 |
| 1.2.4 | 541 | 7/24/2025 |
| 1.2.3 | 605 | 7/23/2025 |
| 1.2.2 | 200 | 7/17/2025 |
| 1.2.1 | 213 | 7/10/2025 |
| 1.2.0 | 215 | 7/9/2025 |
| 1.1.0 | 214 | 7/9/2025 |
| 1.0.2 | 223 | 7/9/2025 |