Wolfix.Server uses the Result Pattern for error handling, providing type-safe, explicit error management without throwing exceptions for business logic failures.
What is the Result Pattern?
The Result Pattern wraps return values and errors in a single type:
Result < Customer > result = customerService . CreateCustomer ( .. .);
if ( result . IsSuccess )
{
Customer customer = result . Value ;
// Work with customer
}
else
{
string error = result . ErrorMessage ;
HttpStatusCode code = result . StatusCode ;
// Handle error
}
Result makes errors explicit and forces callers to handle them - no silent failures or forgotten try-catch blocks.
Result Types
Wolfix.Server provides two result types:
1. Result<TValue>
For operations that return a value:
Shared.Domain/Models/Result.cs
using System . Net ;
namespace Shared . Domain . Models ;
public sealed class Result < TValue >
{
public TValue ? Value { get ; }
public string ? ErrorMessage { get ; }
public bool IsSuccess => ErrorMessage == null ;
public bool IsFailure => ! IsSuccess ;
public HttpStatusCode StatusCode { get ; }
private Result ( TValue value , HttpStatusCode statusCode )
{
Value = value ;
ErrorMessage = null ;
StatusCode = statusCode ;
}
private Result ( string errorMessage , HttpStatusCode statusCode )
{
Value = default ;
ErrorMessage = errorMessage ;
StatusCode = statusCode ;
}
public static Result < TValue > Success ( TValue value , HttpStatusCode statusCode = HttpStatusCode . OK )
=> new ( value , statusCode );
public static Result < TValue > Failure ( string errorMessage , HttpStatusCode statusCode = HttpStatusCode . BadRequest )
=> new ( errorMessage , statusCode );
// Failure from another Result
public static Result < TValue > Failure < TResult >( Result < TResult > failedResult )
{
if ( failedResult . IsSuccess ) throw new ArgumentException ( "Result is success" , nameof ( failedResult ));
return new Result < TValue >( failedResult . ErrorMessage ! , failedResult . StatusCode );
}
// Map for transformations
public TResult Map < TResult >( Func < TValue , TResult > onSuccess , Func < string , TResult > onFailure )
{
return IsSuccess ? onSuccess ( Value ! ) : onFailure ( ErrorMessage ! );
}
}
2. VoidResult
For operations that don’t return a value:
Shared.Domain/Models/VoidResult.cs
public sealed class VoidResult
{
public string ? ErrorMessage { get ; }
public bool IsSuccess => ErrorMessage == null ;
public bool IsFailure => ! IsSuccess ;
public HttpStatusCode StatusCode { get ; }
private VoidResult ( HttpStatusCode statusCode )
{
ErrorMessage = null ;
StatusCode = statusCode ;
}
private VoidResult ( string errorMessage , HttpStatusCode statusCode )
{
ErrorMessage = errorMessage ;
StatusCode = statusCode ;
}
public static VoidResult Success ( HttpStatusCode statusCode = HttpStatusCode . OK )
=> new ( statusCode );
public static VoidResult Failure ( string errorMessage , HttpStatusCode statusCode = HttpStatusCode . BadRequest )
=> new ( errorMessage , statusCode );
public TResult Map < TResult >( Func < TResult > onSuccess , Func < string , TResult > onFailure )
{
return IsSuccess ? onSuccess () : onFailure ( ErrorMessage ! );
}
}
Usage Examples
Domain Layer
Domain entities use Result to validate business rules:
Catalog.Domain/ProductAggregate/Product.cs
public sealed class Product : BaseEntity
{
// Factory method returns Result
public static Result < Product > Create (
string title ,
string description ,
decimal price ,
ProductStatus status ,
Guid categoryId ,
Guid sellerId )
{
if ( string . IsNullOrWhiteSpace ( title ))
return Result < Product >. Failure ( "Title is required" );
if ( price <= 0 )
return Result < Product >. Failure ( "Price must be positive" );
if ( categoryId == Guid . Empty )
return Result < Product >. Failure ( "Category ID is required" );
var product = new Product ( title , description , price , status , categoryId , sellerId );
product . RecalculateBonuses ();
product . FinalPrice = price ;
return Result < Product >. Success ( product , HttpStatusCode . Created );
}
// Behavior method returns VoidResult
public VoidResult ChangePrice ( decimal price )
{
if ( price <= 0 )
return VoidResult . Failure ( "Price must be positive" );
Price = price ;
RecalculateFinalPrice ();
RecalculateBonuses ();
return VoidResult . Success ();
}
// Method that composes Results
public VoidResult AddReview ( string title , string text , uint rating , Guid customerId )
{
Result < Review > createReviewResult = Review . Create ( title , text , rating , this , customerId );
return createReviewResult . Map (
onSuccess : review =>
{
_reviews . Add ( review );
RecalculateAverageRating ();
return VoidResult . Success ();
},
onFailure : errorMessage => VoidResult . Failure ( errorMessage , createReviewResult . StatusCode )
);
}
}
Application Layer
Application services compose multiple Results:
Catalog.Application/Services/ProductService.cs
public class ProductService
{
private readonly IProductRepository _productRepository ;
private readonly ICategoryRepository _categoryRepository ;
private readonly IToxicityService _toxicityService ;
public async Task < Result < ProductDto >> CreateProductAsync (
CreateProductDto dto ,
Guid sellerId ,
CancellationToken ct )
{
// 1. Validate category exists
var categoryExists = await _categoryRepository . ExistsAsync ( dto . CategoryId , ct );
if ( ! categoryExists )
return Result < ProductDto >. Failure (
"Category not found" ,
HttpStatusCode . NotFound
);
// 2. Check for toxic content
Result < bool > toxicityResult = await _toxicityService . IsToxic ( dto . Title , ct );
if ( toxicityResult . IsFailure )
return Result < ProductDto >. Failure ( toxicityResult );
if ( toxicityResult . Value )
return Result < ProductDto >. Failure ( "Title contains inappropriate content" );
// 3. Create domain entity (returns Result)
Result < Product > createResult = Product . Create (
dto . Title ,
dto . Description ,
dto . Price ,
ProductStatus . Draft ,
dto . CategoryId ,
sellerId
);
if ( createResult . IsFailure )
return Result < ProductDto >. Failure ( createResult );
Product product = createResult . Value ! ;
// 4. Save to repository
await _productRepository . AddAsync ( product , ct );
await _productRepository . SaveChangesAsync ( ct );
// 5. Map to DTO and return success
ProductDto productDto = product . ToDto ();
return Result < ProductDto >. Success ( productDto , HttpStatusCode . Created );
}
public async Task < VoidResult > UpdatePriceAsync (
Guid productId ,
decimal newPrice ,
CancellationToken ct )
{
Product ? product = await _productRepository . GetByIdAsync ( productId , ct );
if ( product == null )
return VoidResult . Failure ( "Product not found" , HttpStatusCode . NotFound );
VoidResult changePriceResult = product . ChangePrice ( newPrice );
if ( changePriceResult . IsFailure )
return changePriceResult ;
await _productRepository . UpdateAsync ( product , ct );
await _productRepository . SaveChangesAsync ( ct );
return VoidResult . Success ();
}
}
Presentation Layer (Endpoints)
Endpoints convert Results to HTTP responses:
Catalog.Endpoints/Endpoints/ProductEndpoints.cs
private static async Task < IResult > CreateProduct (
[ FromBody ] CreateProductDto dto ,
[ FromServices ] ProductService productService ,
ClaimsPrincipal user ,
CancellationToken ct )
{
var sellerId = Guid . Parse ( user . FindFirst ( "ProfileId" ) ! . Value );
Result < ProductDto > result = await productService . CreateProductAsync ( dto , sellerId , ct );
return result . IsSuccess
? Results . Created ( $"/api/products/ { result . Value ! . Id } " , result . Value )
: Results . Problem (
detail : result . ErrorMessage ,
statusCode : ( int ) result . StatusCode
);
}
private static async Task < IResult > UpdatePrice (
Guid id ,
[ FromBody ] UpdatePriceDto dto ,
[ FromServices ] ProductService productService ,
CancellationToken ct )
{
VoidResult result = await productService . UpdatePriceAsync ( id , dto . NewPrice , ct );
return result . IsSuccess
? Results . NoContent ()
: Results . Problem (
detail : result . ErrorMessage ,
statusCode : ( int ) result . StatusCode
);
}
Result Chaining
The Map method enables functional composition:
public VoidResult ProcessOrder ( Guid orderId )
{
Result < Order > getOrderResult = orderRepository . GetById ( orderId );
return getOrderResult . Map (
onSuccess : order =>
{
VoidResult validateResult = order . Validate ();
if ( validateResult . IsFailure )
return validateResult ;
VoidResult chargeResult = paymentService . Charge ( order . TotalAmount );
if ( chargeResult . IsFailure )
return chargeResult ;
order . MarkAsPaid ();
return VoidResult . Success ();
},
onFailure : error => VoidResult . Failure ( error )
);
}
Propagating Failures
Result provides helper methods to propagate failures across layers:
// Application service
public async Task < Result < OrderDto >> CreateOrderAsync ( CreateOrderDto dto , CancellationToken ct )
{
// Get customer
Result < Customer > customerResult = await customerService . GetCustomerAsync ( dto . CustomerId , ct );
if ( customerResult . IsFailure )
return Result < OrderDto >. Failure ( customerResult ); // Propagate failure
// Create order
Result < Order > createResult = Order . Create ( customerResult . Value ! , dto . Items );
if ( createResult . IsFailure )
return Result < OrderDto >. Failure ( createResult ); // Propagate failure
// Save and return
await orderRepository . AddAsync ( createResult . Value ! , ct );
return Result < OrderDto >. Success ( createResult . Value ! . ToDto ());
}
Integration with External Services
External service calls return Results:
Catalog.Infrastructure/Services/ToxicityService.cs
internal sealed class ToxicityService : IToxicityService
{
private readonly HttpClient _httpClient ;
public async Task < Result < bool >> IsToxic ( string text , CancellationToken ct )
{
try
{
var payload = new { text };
var response = await _httpClient . PostAsJsonAsync ( "check" , payload , ct );
response . EnsureSuccessStatusCode ();
var result = await response . Content . ReadFromJsonAsync < bool >( ct );
return Result < bool >. Success ( result );
}
catch ( HttpRequestException ex )
{
return Result < bool >. Failure (
$"Toxicity service error: { ex . Message } " ,
HttpStatusCode . ServiceUnavailable
);
}
catch ( Exception ex )
{
return Result < bool >. Failure (
$"Unexpected error: { ex . Message } " ,
HttpStatusCode . InternalServerError
);
}
}
}
Order.Infrastructure/Services/StripePaymentService.cs
internal sealed class StripePaymentService : IPaymentService < StripePaymentResponse >
{
private readonly StripeClient _stripeClient ;
public async Task < Result < StripePaymentResponse >> PayAsync (
decimal amount ,
string currency ,
string customerEmail ,
CancellationToken ct )
{
try
{
var options = new PaymentIntentCreateOptions
{
Amount = ( long )( amount * 100 ),
Currency = currency ,
ReceiptEmail = customerEmail ,
PaymentMethodTypes = [ "card" ]
};
PaymentIntentService service = new ( _stripeClient );
PaymentIntent intent = await service . CreateAsync ( options , cancellationToken : ct );
return Result < StripePaymentResponse >. Success ( new StripePaymentResponse
{
PaymentIntentId = intent . Id ,
ClientSecret = intent . ClientSecret
});
}
catch ( StripeException ex )
{
return Result < StripePaymentResponse >. Failure (
$"Stripe error: { ex . Message } " ,
HttpStatusCode . InternalServerError
);
}
}
}
Benefits
Explicit Errors Errors are visible in method signatures
Type Safety Compiler ensures error handling
No Silent Failures Can’t ignore errors - must check IsSuccess
Composability Easy to chain operations with Map
HTTP Integration StatusCode property maps directly to HTTP responses
Testability Easy to test both success and failure paths
Result vs Exceptions
When to Use Result
Business rule violations
Expected failure conditions
Validation errors
Domain logic failures
// ✅ Use Result for business logic
public VoidResult ChangePrice ( decimal price )
{
if ( price <= 0 )
return VoidResult . Failure ( "Price must be positive" );
Price = price ;
return VoidResult . Success ();
}
When to Use Exceptions
Unexpected errors (infrastructure failures)
Programming errors (null reference, index out of range)
Configuration errors
System failures
// ✅ Use exceptions for unexpected errors
public class ProductRepository
{
public async Task < Product ?> GetByIdAsync ( Guid id , CancellationToken ct )
{
// Let EF Core throw DbUpdateException, SqlException, etc.
return await _context . Products . FirstOrDefaultAsync ( p => p . Id == id , ct );
}
}
Don’t use Result for everything - exceptions are still appropriate for truly exceptional situations.
Testing with Result
Catalog.Tests/Domain/ProductTests.cs
public class ProductTests
{
[ Fact ]
public void Create_WithValidData_ShouldReturnSuccess ()
{
// Arrange
var title = "Test Product" ;
var price = 99.99m ;
// Act
Result < Product > result = Product . Create (
title , "Description" , price ,
ProductStatus . Draft , Guid . NewGuid (), Guid . NewGuid ()
);
// Assert
Assert . True ( result . IsSuccess );
Assert . NotNull ( result . Value );
Assert . Equal ( title , result . Value . Title );
Assert . Equal ( HttpStatusCode . Created , result . StatusCode );
}
[ Theory ]
[ InlineData ( 0 )]
[ InlineData ( - 1 )]
[ InlineData ( - 100 )]
public void Create_WithInvalidPrice_ShouldReturnFailure ( decimal price )
{
// Act
Result < Product > result = Product . Create (
"Title" , "Description" , price ,
ProductStatus . Draft , Guid . NewGuid (), Guid . NewGuid ()
);
// Assert
Assert . True ( result . IsFailure );
Assert . Null ( result . Value );
Assert . Contains ( "positive" , result . ErrorMessage );
Assert . Equal ( HttpStatusCode . BadRequest , result . StatusCode );
}
}
Best Practices
Always Check IsSuccess
Never access Value without checking IsSuccess first: var result = service . CreateProduct ( .. .);
if ( result . IsSuccess )
{
var product = result . Value ; // Safe
}
Use Appropriate Status Codes
Match HTTP status codes to error types: // Not found
return Result < Product >. Failure ( "Product not found" , HttpStatusCode . NotFound );
// Validation error
return Result < Product >. Failure ( "Invalid price" , HttpStatusCode . BadRequest );
// Conflict
return Result < Product >. Failure ( "Product already exists" , HttpStatusCode . Conflict );
Provide Clear Error Messages
Make errors actionable for API consumers: // ❌ Bad
return VoidResult . Failure ( "Invalid" );
// ✅ Good
return VoidResult . Failure ( "Price must be greater than 0" );
Use Map for Transformations
Leverage Map for cleaner code: return result . Map (
onSuccess : value = > ProcessValue ( value ),
onFailure : error => HandleError ( error )
);
Next Steps
Development Guide Start building with Result pattern
API Reference Explore API endpoints using Result