FluentResults Integration
Use FluentResults instead of exceptions for error handling.
Overview
FluentResults provides a functional approach to error handling, returning Result<T> objects instead of throwing exceptions. This makes error handling explicit and composable.
Installation
dotnet add package FluentResults
Configuration
Create a wrapper service that returns Result<T> from DynamoDB operations:
using FluentResults;
using Oproto.FluentDynamoDb;
public class ResultDynamoDbService<TEntity> where TEntity : class
{
private readonly IDynamoDbTable _table;
public ResultDynamoDbService(IDynamoDbTable table)
{
_table = table;
}
public async Task<Result<TEntity?>> GetAsync(string pk, string? sk = null)
{
try
{
var item = await _table.Get<TEntity>()
.WithKey("pk", pk)
.WithKey("sk", sk ?? pk)
.GetItemAsync();
return Result.Ok(item);
}
catch (Exception ex)
{
return Result.Fail<TEntity?>(new ExceptionalError(ex));
}
}
public async Task<Result> PutAsync(TEntity item)
{
try
{
await _table.Put<TEntity>()
.WithItem(item)
.PutAsync();
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new ExceptionalError(ex));
}
}
}
Result Types
Success Results
// Get operation returning Result<T>
var result = await service.GetAsync("user123");
if (result.IsSuccess)
{
var user = result.Value;
Console.WriteLine($"Found user: {user?.Name}");
}
Failure Results
var result = await service.GetAsync("nonexistent");
if (result.IsFailed)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Error: {error.Message}");
}
}
Custom Error Types
public class NotFoundError : Error
{
public string EntityType { get; }
public string Key { get; }
public NotFoundError(string entityType, string key)
: base($"{entityType} with key '{key}' was not found")
{
EntityType = entityType;
Key = key;
}
}
public class ConflictError : Error
{
public ConflictError(string message) : base(message) { }
}
Error Handling
Repository Pattern with Results
public class UserRepository
{
private readonly UserTable _table;
public UserRepository(UserTable table)
{
_table = table;
}
public async Task<Result<User>> GetUserAsync(string userId)
{
try
{
var user = await _table.Users.GetAsync(userId);
if (user == null)
{
return Result.Fail<User>(new NotFoundError("User", userId));
}
return Result.Ok(user);
}
catch (Exception ex)
{
return Result.Fail<User>(new ExceptionalError(ex));
}
}
public async Task<Result<List<User>>> QueryActiveUsersAsync()
{
try
{
var users = await _table.Users.Query()
.Where(x => x.Status == "active")
.ToListAsync();
return Result.Ok(users);
}
catch (Exception ex)
{
return Result.Fail<List<User>>(new ExceptionalError(ex));
}
}
public async Task<Result> CreateUserAsync(User user)
{
try
{
await _table.Users.Put(user)
.Where(x => x.Pk.AttributeNotExists())
.PutAsync();
return Result.Ok();
}
catch (ConditionalCheckFailedException)
{
return Result.Fail(new ConflictError($"User {user.UserId} already exists"));
}
catch (Exception ex)
{
return Result.Fail(new ExceptionalError(ex));
}
}
public async Task<Result> UpdateUserAsync(string userId, string newName)
{
try
{
await _table.Users.Update(userId)
.Set(x => new { Name = newName })
.Where(x => x.Pk.AttributeExists())
.UpdateAsync();
return Result.Ok();
}
catch (ConditionalCheckFailedException)
{
return Result.Fail(new NotFoundError("User", userId));
}
catch (Exception ex)
{
return Result.Fail(new ExceptionalError(ex));
}
}
public async Task<Result> DeleteUserAsync(string userId)
{
try
{
await _table.Users.DeleteAsync(userId);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new ExceptionalError(ex));
}
}
}
Chaining Results
public async Task<Result<Order>> CreateOrderWithItemsAsync(Order order, List<OrderItem> items)
{
// Create order
var orderResult = await CreateOrderAsync(order);
if (orderResult.IsFailed)
{
return orderResult.ToResult<Order>();
}
// Create order items
foreach (var item in items)
{
var itemResult = await CreateOrderItemAsync(item);
if (itemResult.IsFailed)
{
// Rollback order on failure
await DeleteOrderAsync(order.OrderId);
return itemResult.ToResult<Order>();
}
}
return Result.Ok(order);
}
Benefits
- Explicit error handling - Errors are part of the return type, not hidden in exceptions
- Better control flow - Use pattern matching and LINQ-style operations on results
- Functional programming style - Compose operations with
Bind,Map, and other combinators - No exception overhead - Avoid the performance cost of throwing and catching exceptions
- Self-documenting - Method signatures clearly indicate possible failure modes