Result Pattern
Vulthil.Results provides a railway-oriented Result / Result<T> type that models success and failure explicitly, eliminating the need to throw exceptions for expected error conditions.
Core Types
| Type | Description |
|---|---|
Result |
Represents success or failure without a value |
Result<T> |
Represents success with a value of type T, or failure |
Error |
Structured error with Code, Description, and ErrorType |
ValidationError |
An Error subtype that carries multiple inner errors |
Creating Results
// Success
Result ok = Result.Success();
Result<int> typed = Result.Success(42);
// Failure
Error error = Error.NotFound("User.NotFound", "User was not found");
Result<User> failed = Result.Failure<User>(error);
// Validation failure
var validation = new ValidationError([
Error.Problem("Email.Required", "Email is required"),
Error.Problem("Name.TooLong", "Name exceeds 100 characters")
]);
Result<User> invalid = Result.ValidationFailure<User>(validation);
Error Classifications
ErrorType determines how the error maps to HTTP status codes in the API layer:
| ErrorType | Factory Method | HTTP Status |
|---|---|---|
Failure |
Error.Failure(...) |
500 Internal Server Error |
NotFound |
Error.NotFound(...) |
404 Not Found |
Problem |
Error.Problem(...) |
500 Internal Server Error |
Conflict |
Error.Conflict(...) |
409 Conflict |
Validation |
ValidationError(...) |
400 Bad Request |
Defining Domain Errors
Keep domain errors as static fields so they are reusable and discoverable:
public static class UserErrors
{
public static readonly Error NotFound =
Error.NotFound("User.NotFound", "User was not found");
public static readonly Error EmailTaken =
Error.Conflict("User.EmailTaken", "Email is already in use");
}
Functional Extensions
The library ships extension methods that let you compose operations without manual if/else branching.
Bind – chain dependent operations
Result<Order> result = await GetUser(userId)
.BindAsync(user => CreateOrder(user));
If GetUser fails, the error propagates and CreateOrder is never called.
Map – transform the success value
Result<string> email = getUser
.Map(user => user.Email);
Tap – execute a side-effect without changing the result
Result<User> result = await CreateUser(command)
.TapAsync(user => logger.LogInformation("Created {Id}", user.Id));
Match – branch on success/failure
string message = result.Match(
onSuccess: user => $"Welcome, {user.Name}",
onFailure: error => $"Error: {error.Description}");
All extensions have synchronous and asynchronous overloads so they compose naturally with Task<Result<T>>.
Mapping to HTTP Responses
Vulthil.SharedKernel.Api provides helpers that turn a Result into the correct HTTP response:
// Minimal API endpoint
app.MapGet("/users/{id:guid}", async (Guid id, ISender sender) =>
{
var result = await sender.SendAsync(new GetUserQuery(id));
return result.ToIResult();
});
// Controller-based endpoint
public async Task<IActionResult> Get(Guid id)
{
var result = await _sender.SendAsync(new GetUserQuery(id));
return result.ToActionResult(this);
}