A simple, lightweight, and powerful implementation of the Result Pattern for C# that provides elegant error handling and success result management. Designed for building robust, maintainable applications with clear error flow control.
| Package | Description | Documentation |
|---|---|---|
| ResultFlow | Core Result pattern library with zero dependencies | 📖 This Document |
| ResultFlow.AspNetCore | ASP.NET Core integration with automatic HTTP status code mapping | 📖 View Documentation |
| ResultFlow.FluentValidation | FluentValidation seamless integration for validation results | 📖 View Documentation |
Future Extensions:
- 🔮 ResultFlow.Logging - Structured logging integration (coming in v3.0)
- 🔮 ResultFlow.Testing - Unit testing assertions (coming in v3.0)
- ✅ Type-Safe Result Pattern - Strongly-typed success and failure handling without exceptions for flow control
- ✅ Immutable Design -
readonly structimplementation for thread-safe, memory-efficient operations - ✅ Functional API - Comprehensive functional programming support with
Map,Bind,Filter,Match, andTap - ✅ Pattern Matching - Full C# pattern matching support for elegant control flow
- ✅ Non-Generic Result - Support for void operations that don't return values
- ✅ Flexible Error Handling - Rich error metadata, tracing, and exception wrapping for debugging
- ✅ Enterprise-Ready - Optimized for ASP.NET Core with seamless ActionResult conversion
- ✅ Zero Dependencies - Core library has no external dependencies
- ✅ MIT Licensed - Free for personal and commercial use
Install the NuGet package:
dotnet add package ResultFlowOr via Package Manager:
Install-Package ResultFlowusing ResultFlow.Results;
using ResultFlow.Errors;
// Success case
var userResult = Result<User>.Ok(new User { Id = 1, Name = "John Doe" });
// Failure case
var errorResult = Result<User>.Failed(
NotFoundError.WithDefaults("User not found")
);
// Check status
if (userResult.IsOk)
{
var user = userResult.Value;
Console.WriteLine($"User: {user.Name}");
}
if (errorResult.HasError)
{
Console.WriteLine($"Error: {errorResult.Error?.Message}");
}// Match with return value
var message = userResult.Match(
onSuccess: user => $"Welcome, {user.Name}!",
onFailure: error => $"Error: {error.Message}"
);
Console.WriteLine(message);
// Match with side effects
userResult.Match(
onSuccess: user => Console.WriteLine($"Found user: {user.Name}"),
onFailure: error => Console.WriteLine($"Error: {error.Message}")
);// For operations that don't return a value
public Result DeleteUser(int id)
{
var user = _database.FindUser(id);
if (user == null)
return Result.Failed(NotFoundError.WithDefaults("User not found"));
_database.Delete(user);
return Result.Ok();
}
// Usage
var deleteResult = DeleteUser(1);
deleteResult.Match(
onSuccess: () => Console.WriteLine("User deleted successfully"),
onFailure: error => Console.WriteLine($"Error: {error.Message}")
);ResultFlow provides multiple ways to create and handle errors:
// Simple, quick error creation
var notFoundError = PredefinedErrors.NotFound("User");
var badRequestError = PredefinedErrors.BadRequest("Invalid input");
var conflictError = PredefinedErrors.Conflict("Email already exists");
var authError = PredefinedErrors.Unauthorized();
var validationError = PredefinedErrors.ValidationFailed("Multiple validation errors");
var serverError = PredefinedErrors.InternalError("Database connection failed");
return Result<User>.Failed(notFoundError);// NotFoundError with context
var error = NotFoundError.ForResource("Product", resourceId: "PROD-123");
// Message: "The requested Product was not found."
// Metadata: { resourceName: "Product", resourceId: "PROD-123" }
// BadRequestError with field context
var error = BadRequestError.ForInvalidParameter(
parameterName: "email",
reason: "Invalid email format",
providedValue: "not-an-email"
);
// Message: "The parameter 'email' is invalid: Invalid email format"
// Metadata: { parameterName: "email", providedValue: "not-an-email" }
// ConflictError with version info
var error = ConflictError.ForVersionMismatch(
resourceName: "Document",
expectedVersion: "v1",
currentVersion: "v2"
);
// Message: "The Document has been modified. Expected version: v1, Current version: v2."
// ForbiddenError with role info
var error = ForbiddenError.ForMissingRole(
requiredRole: "Admin",
userRole: "User"
);
// Message: "This operation requires the 'Admin' role."
// Metadata: { requiredRole: "Admin", userRole: "User" }
// InternalServerError with exception
var error = InternalServerError.FromException(
dbException,
message: "Database query failed"
);
// Automatically includes exception type, message, and stack trace in metadatavar error = ErrorBuilder
.Create("PAYMENT_FAILED", "Payment processing failed")
.WithDetails("Credit card was declined by the payment gateway")
.AddMetadata("orderId", order.Id)
.AddMetadata("amount", order.Total)
.AddMetadata("paymentGateway", "Stripe")
.AddMetadata("timestamp", DateTime.UtcNow)
.AddMetadata("retryable", true)
.WithException(stripeException)
.Build();
return Result<PaymentResult>.Failed(error);var userIdResult = Result<int>.Ok(42);
var userResult = userIdResult
.Map(id => _userService.GetUserById(id))
.Map(user => user.Email)
.Map(email => new EmailDto { Address = email });
// Chain transformations with error propagation
// If any step fails, error is returned immediatelypublic Result<User> GetUser(int id)
{
if (id <= 0)
return Result<User>.Failed(
BadRequestError.ForInvalidParameter("id", "Must be positive")
);
var user = _database.FindUser(id);
return user != null
? Result<User>.Ok(user)
: Result<User>.Failed(NotFoundError.ForResource("User", id.ToString()));
}
public Result<string> GetUserEmail(int id)
{
return GetUser(id)
.Bind(user => ValidateUserAsync(user))
.Bind(user => AuthorizeUserAsync(user))
.Map(user => user.Email);
}
// Result: Ok("john@example.com") or any error from the chainvar ageResult = Result<int>.Ok(25);
var validAge = ageResult.Filter(
age => age >= 18,
new ValidationError("UNDERAGE", "User must be 18 or older")
);
if (validAge.IsOk)
{
Console.WriteLine("User is old enough");
}var userResult = Result<User>.Ok(new User { Id = 1, Name = "John" });
var result = userResult
.Tap(user => Console.WriteLine($"Processing user: {user.Name}"))
.Tap(user => _logger.LogInformation($"User ID: {user.Id}"))
.Tap(user => _cache.Set($"user:{user.Id}", user))
.TapError(error => _logger.LogError($"Error: {error.Message}"))
.Map(user => user.Email);
// Tap executes side effects without changing the resultvar result = ValidateUser(user)
.Then(user => SaveUserToDatabase(user))
.Then(user => SendWelcomeEmail(user))
.Then(user => LogUserCreation(user));
// Then chains operations sequentially
// Short-circuits on first failureResultFlow provides seamless support for async operations through Task<Result<T>> and Task<VoidResult> extensions. This allows you to chain async methods without manually awaiting them at each step.
public async Task<Result<UserDto>> GetUserDtoAsync(int userId)
{
return await _repository.GetUserAsync(userId)
.BindAsync(user => _validator.ValidateAsync(user))
.MapAsync(user => _mapper.MapToDtoAsync(user));
}public async Task<Result<User>> CreateUserAsync(User user)
{
return await ValidateUserAsync(user)
.TapAsync(u => _logger.LogInformationAsync($"Creating user {u.Name}"))
.TapAsync(u => _repository.AddAsync(u))
.TapErrorAsync(e => _logger.LogErrorAsync($"Failed: {e.Message}"));
}var result = await GetUserAsync(1);
await result.MatchAsync(
onSuccess: async user => await SendEmailAsync(user),
onFailure: async error => await LogErrorAsync(error)
);var userResult = GetUser(123);
// Option 1: Get value or default
var user = userResult.GetValueOrDefault(new User { Id = 0, Name = "Unknown" });
// Option 2: Get value or execute fallback
var user = userResult.GetValueOrElse(error =>
{
_logger.LogError($"Failed to get user: {error.Message}");
return new User { Id = 0, Name = "Unknown" };
});
// Option 3: Get value or throw exception
var user = userResult.GetValueOrThrow();
// Throws InvalidOperationException if failed
// Option 4: Get value with custom exception
var user = userResult.GetValueOrThrow(error =>
new UserNotFoundException($"User not found: {error.Message}", error.InnerException)
);ResultFlow includes comprehensive HTTP error types with factory methods:
| Error Type | HTTP Code | Factory Methods | Use Case |
|---|---|---|---|
| BadRequestError | 400 | WithDefaults(), ForInvalidParameter(), ForMissingField(), ForInvalidFormat() |
Invalid request parameters or format |
| UnauthorizedError | 401 | WithDefaults(), ForReason() |
Missing or invalid authentication |
| ForbiddenError | 403 | WithDefaults(), ForMissingRole(), ForMissingPermission() |
User lacks required permissions |
| NotFoundError | 404 | WithDefaults(), ForResource(), ByIdentifier() |
Resource not found |
| ConflictError | 409 | WithDefaults(), ForDuplicateResource(), ForVersionMismatch(), ForStateConflict() |
Resource conflict or duplicate |
| ValidationError | 422 | WithDefaults(), ForField() |
Validation rule violations |
| InternalServerError | 500 | WithDefaults(), FromException(), ForOperation(), WithCode() |
Server-side errors |
// Using ErrorCodes constants
var error = new Error(
code: ErrorCodes.BadRequest.InvalidParameter,
message: "Email parameter is invalid",
details: "Email must follow pattern: user@example.com",
metadata: new Dictionary<string, object>
{
{ "fieldName", "email" },
{ "expectedFormat", "user@example.com" },
{ "providedValue", "invalid-email" }
}
);
// Using ErrorBuilder for complex scenarios
var error = ErrorBuilder
.Create(ErrorCodes.Custom.BusinessLogicError, "Order cannot be processed")
.WithDetails("Customer credit limit exceeded")
.AddMetadata("customerId", 123)
.AddMetadata("creditLimit", 5000)
.AddMetadata("orderTotal", 6000)
.AddMetadata("exceededBy", 1000)
.Build();public class UserService
{
private readonly IUserRepository _repository;
private readonly ILogger<UserService> _logger;
public async Task<Result<User>> GetUserByIdAsync(int id)
{
if (id <= 0)
{
_logger.LogWarning("Invalid user ID: {UserId}", id);
return Result<User>.Failed(
BadRequestError.ForInvalidParameter("id", "Must be positive", id)
);
}
var user = await _repository.FindByIdAsync(id);
if (user == null)
{
_logger.LogWarning("User not found: {UserId}", id);
return Result<User>.Failed(
NotFoundError.ByIdentifier("User", id)
);
}
return Result<User>.Ok(user);
}
public async Task<Result<User>> CreateUserAsync(CreateUserRequest request)
{
// Validate input
if (string.IsNullOrWhiteSpace(request.Email))
return Result<User>.Failed(
BadRequestError.ForMissingField("Email")
);
if (!IsValidEmail(request.Email))
return Result<User>.Failed(
BadRequestError.ForInvalidParameter("Email", "Invalid format", request.Email)
);
// Check for duplicates
var existing = await _repository.FindByEmailAsync(request.Email);
if (existing != null)
return Result<User>.Failed(
ConflictError.ForDuplicateResource("Email", request.Email)
);
// Create user
try
{
var user = new User
{
Email = request.Email,
Name = request.Name,
CreatedAt = DateTime.UtcNow
};
await _repository.AddAsync(user);
_logger.LogInformation("User created: {UserId}", user.Id);
return Result<User>.Ok(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating user");
return Result<User>.Failed(
InternalServerError.FromException(ex, "Error creating user")
);
}
}
public async Task<Result> DeleteUserAsync(int id)
{
var user = await _repository.FindByIdAsync(id);
if (user == null)
return Result.Failed(
NotFoundError.ByIdentifier("User", id)
);
try
{
await _repository.DeleteAsync(user);
_logger.LogInformation("User deleted: {UserId}", id);
return Result.Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting user: {UserId}", id);
return Result.Failed(
InternalServerError.ForOperation("DeleteUser", ex)
);
}
}
private bool IsValidEmail(string email)
=> email.Contains("@") && email.Contains(".");
}public async Task<Result<OrderDto>> ProcessOrderAsync(int orderId)
{
return await GetOrderAsync(orderId)
.Bind(order => ValidateOrderAsync(order))
.Bind(order => CheckInventoryAsync(order))
.Bind(order => ProcessPaymentAsync(order))
.Bind(order => CreateShipmentAsync(order))
.Bind(order => SendConfirmationEmailAsync(order))
.Tap(order => _logger.LogInformation("Order processed: {OrderId}", order.Id))
.TapError(error => _logger.LogError("Order processing failed: {Error}", error.Message))
.Map(order => _mapper.Map<OrderDto>(order));
}
private async Task<Result<Order>> GetOrderAsync(int id)
{
var order = await _repository.GetOrderAsync(id);
return order != null
? Result<Order>.Ok(order)
: Result<Order>.Failed(NotFoundError.ForResource("Order", id.ToString()));
}
private async Task<Result<Order>> ValidateOrderAsync(Order order)
{
if (order.Items.Count == 0)
return Result<Order>.Failed(
ValidationError.WithDefaults("Order must contain at least one item")
);
return Result<Order>.Ok(order);
}
private async Task<Result<Order>> CheckInventoryAsync(Order order)
{
foreach (var item in order.Items)
{
var stock = await _inventoryService.GetStockAsync(item.ProductId);
if (stock < item.Quantity)
return Result<Order>.Failed(
ConflictError.WithDefaults($"Insufficient stock for product {item.ProductId}")
);
}
return Result<Order>.Ok(order);
}
private async Task<Result<Order>> ProcessPaymentAsync(Order order)
{
try
{
await _paymentService.ChargeAsync(order.CustomerId, order.Total);
return Result<Order>.Ok(order);
}
catch (PaymentException ex)
{
return Result<Order>.Failed(
InternalServerError.ForOperation("ProcessPayment", ex)
);
}
}
// ... more methodspublic Result<User> CreateUser(UserRequest request)
{
return ValidateUserRequest(request)
.Filter(
user => user.Age >= 18,
ValidationError.WithDefaults("User must be 18 years old")
)
.Filter(
user => !user.Email.Contains("invalid"),
BadRequestError.ForInvalidParameter("Email", "Invalid email domain")
)
.Tap(user => _repository.Add(user))
.Tap(user => _logger.LogInformation("User created: {UserId}", user.Id));
}
private Result<User> ValidateUserRequest(UserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
return Result<User>.Failed(
BadRequestError.ForMissingField("Name")
);
if (string.IsNullOrWhiteSpace(request.Email))
return Result<User>.Failed(
BadRequestError.ForMissingField("Email")
);
return Result<User>.Ok(new User
{
Name = request.Name,
Email = request.Email,
Age = request.Age
});
}// Factory Methods
Result<T> Ok(T value) // Create success result
Result<T> Failed(Error error) // Create failure result
// Properties
T? Value // Success value
Error? Error // Error information
bool IsOk // Check if successful
bool HasError // Check if failed
// Functional Methods
Result<TResult> Map<TResult>(Func<T, TResult> map)
Result<TResult> Bind<TResult>(Func<T, Result<TResult>> bind)
Result<TResult> Filter(Func<T, bool> predicate, Error error)
Result<T> Tap(Action<T> action)
Result<T> TapError(Action<Error> action)
Result<TResult> Then<TResult>(Result<TResult> other)
Result<TResult> Then<TResult>(Func<T, Result<TResult>> then)
// Pattern Matching
TResult Match<TResult>(Func<T, TResult> onSuccess, Func<Error, TResult> onFailure)
void Match(Action<T> onSuccess, Action<Error> onFailure)
// Value Retrieval
T? GetValueOrDefault(T? defaultValue = default)
T? GetValueOrElse(Func<Error, T?> onFailure)
T? GetValueOrThrow()
T? GetValueOrThrow(Func<Error, Exception> exceptionFactory)
// Equality
bool Equals(Result<T> other)
override int GetHashCode()
override string ToString()// Factory Methods
Result Ok() // Create success result
Result Failed(Error error) // Create failure result
// Properties
Error? Error // Error information
bool IsOk // Check if successful
bool HasError // Check if failed
// Functional Methods
Result Tap(Action action)
Result TapError(Action<Error> action)
// Pattern Matching
void Match(Action onSuccess, Action<Error> onFailure)
TResult Match<TResult>(Func<TResult> onSuccess, Func<Error, TResult> onFailure)
// Utility
Error? GetErrorOrThrow()
override string ToString()// Properties
string Code // Error code identifier
string Message // Human-readable message
string? Details // Additional details
Dictionary<string, object>? Metadata // Custom metadata
Exception? InnerException // Underlying exceptionErrorBuilder WithCode(string code)
ErrorBuilder WithMessage(string message)
ErrorBuilder WithDetails(string details)
ErrorBuilder WithMetadata(Dictionary<string, object> metadata)
ErrorBuilder AddMetadata(string key, object value)
ErrorBuilder AddMetadataRange(Dictionary<string, object> metadata)
ErrorBuilder WithException(Exception exception)
ErrorBuilder ClearMetadata()
Error Build()
TError Build<TError>() where TError : Error, new()
static ErrorBuilder Create(string code, string message)
static ErrorBuilder Empty()Extensions for Task<Result<T>> and Task<VoidResult>:
// Async Transformations
Task<Result<TNew>> MapAsync<T, TNew>(this Task<Result<T>>, Func<T, Task<TNew>>)
Task<Result<TNew>> BindAsync<T, TNew>(this Task<Result<T>>, Func<T, Task<Result<TNew>>>)
// Async Side Effects
Task<Result<T>> TapAsync<T>(this Task<Result<T>>, Func<T, Task>)
Task<Result<T>> TapErrorAsync<T>(this Task<Result<T>>, Func<Error, Task>)
// Async Pattern Matching
Task<TResult> MatchAsync<T, TResult>(this Task<Result<T>>, Func<T, Task<TResult>>, Func<Error, Task<TResult>>)Contributions are welcome! Please feel free to submit issues or pull requests on GitHub.
git clone https://github.com/saidshl/ResultFlow.git
cd ResultFlow
dotnet build
dotnet testFor questions, issues, or suggestions:
- 🐛 Create an Issue
- 💡 Start a Discussion
- 📦 Visit Repository
Made with ❤️ by Said Souhayel
Last updated: 2025-11-23 10:13:05 UTC