When developing modern applications, handling operation outcomes effectively presents several challenges:
- Inconsistent Error Handling: Different parts of the application may handle errors in different ways, leading to inconsistent error reporting and handling.
- Context Loss: Important error context and details can be lost when exceptions are caught and rethrown up the call stack.
- Mixed Concerns: Business logic errors often get mixed with technical exceptions, making it harder to handle each appropriately.
- Pagination Complexity: Managing paginated data with associated metadata adds complexity to result handling.
- Type Safety: Maintaining type safety while handling both successful and failed operations can be challenging.
- Error Propagation: Propagating errors through multiple layers of the application while preserving context.
The Result pattern implementation provides a comprehensive solution by:
- Providing a standardized way to handle operation outcomes
- Encapsulating success/failure status, messages, and errors in a single object
- Supporting generic result types for operations that return values
- Offering specialized support for paginated results
- Enabling strongly-typed error handling
- Maintaining immutability with a fluent interface design
A type-safe Result pattern implementation for explicit success/failure handling with optional functional extensions.
The Result pattern consists of three primary classes in hierarchy: Result
provides base
success/failure tracking with message and error collections, ResultT
adds generic type support for
strongly-typed value handling, and PagedResultT
extends this for collection scenarios with
pagination metadata. Each maintains a fluent interface with factory methods (Success()
,
Failure()
). Error handling is supported through IResultError
, enabling custom error types across
the hierarchy.
The pattern can be enhanced with optional functional extensions like Map/Bind
for transformations,
Tap
for side effects, and Filter/Unless
for conditionals and more.
See Appendix B: Functional Extensions for an overview
of the available functional operations.
classDiagram
class IResultError {
<<interface>>
+string Message
}
class ResultErrorBase {
<<abstract>>
+string Message
+ResultErrorBase(string message)
}
class IResult {
<<interface>>
+IReadOnlyList Messages
+IReadOnlyList Errors
+bool IsSuccess
+bool IsFailure
+bool HasError()
}
class IResultT {
<<interface>>
+T Value
}
class Result {
-List messages
-List errors
-bool success
+bool IsSuccess
+bool IsFailure
+Result WithMessage(string)
+Result WithMessages(IEnumerable)
+Result WithError(IResultError)
+Result WithErrors(IEnumerable)
+Result For()
+static Result Success()
+static Result Failure()
}
class ResultT {
-List messages
-List errors
-bool success
+T Value
+ResultT WithMessage(string)
+ResultT WithMessages(IEnumerable)
+ResultT WithError(IResultError)
+ResultT WithErrors(IEnumerable)
+Result For()
+static ResultT Success(T value)
+static ResultT Failure()
}
class PagedResultT {
+int CurrentPage
+int TotalPages
+long TotalCount
+int PageSize
+bool HasPreviousPage
+bool HasNextPage
+static PagedResultT Success(IEnumerable, long, int, int)
+static PagedResultT Failure()
}
IResultError <|.. ResultErrorBase
IResult <|-- IResultT
IResult <|.. Result
IResultT <|.. ResultT
ResultT <|-- PagedResultT
Result ..> IResultError : uses
ResultT ..> IResultError : uses
PagedResultT ..> IResultError : uses
IResult ..> IResultError : contains
The Result pattern is particularly useful in the following scenarios:
- Service Layer Operations
- Handling business rule validations
- Processing complex operations with multiple potential failure points
- Returning domain-specific errors
- Data Access Operations
- Managing database operations
- Handling entity not found scenarios
- Dealing with validation errors
- API Endpoints
- Returning paginated data
- Handling complex operation outcomes
- Providing detailed error information
- Complex Workflows
- Managing multi-step processes
- Handling conditional operations
- Aggregating errors from multiple sources
// Creating success results
var success = Result.Success();
var successWithMessage = Result.Success("Operation completed successfully");
// Creating failure results
var failure = Result.Failure("Operation failed")
.WithError<ValidationError>();
// Checking result status
if (success.IsSuccess)
{
// Handle success
}
if (failure.IsFailure)
{
// Handle failure
}
// Error handling
if (failure.HasError<ValidationError>())
{
// Handle specific error type
}
// Paged result
var pagedResult = PagedResultT<Item>.Success(
items, totalCount: 100, page: 1, pageSize: 10
);
public class UserService
{
private readonly IUserRepository repository;
public UserService(IUserRepository repository)
{
this.repository = repository;
}
public Result<User> GetUserById(int id)
{
var user = this.repository.FindById(id);
if (user == null)
{
return Result<User>.Failure()
.WithError<NotFoundResultError>()
.WithMessage($"User {id} not found");
}
return Result<User>.Success(user);
}
}
public class ValidationError : ResultErrorBase
{
public ValidationError(string message) : base(message)
{
}
}
public class ValidationService
{
public Result ValidateData(DataModel model)
{
var result = Result.Success();
if (!this.IsValid(model))
{
result = Result.Failure()
.WithError(new ValidationError("Invalid input"))
.WithMessage("Validation failed");
}
// Check for specific errors
if (result.HasError<ValidationError>())
{
// Handle validation error
}
if (result.HasError<ValidationError>(out var validationErrors))
{
foreach (var error in validationErrors)
{
Console.WriteLine(error.Message);
}
}
return result;
}
private bool IsValid(DataModel model)
{
// Validation logic
return true;
}
}
public class WorkflowService
{
public Result ProcessWorkflow()
{
var result = Result.Success()
.WithMessage("Step 1 completed")
.WithMessage("Step 2 completed");
var messages = new[] { "Process started", "Process completed" };
result = Result.Success()
.WithMessages(messages);
foreach (var message in result.Messages)
{
Console.WriteLine(message);
}
return result;
}
}
public class ProductService
{
private readonly IProductRepository repository;
private readonly ILogger<ProductService> logger;
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger)
{
this.repository = repository;
this.logger = logger;
}
public async Task<PagedResult<Product>> GetProductsAsync(int page = 1, int pageSize = 10)
{
try
{
var totalCount = await this.repository.CountAsync();
var products = await this.repository.GetPageAsync(page, pageSize);
return PagedResult<Product>.Success(
products,
totalCount,
page,
pageSize);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Failed to get products");
return PagedResult<Product>.Failure()
.WithError(new ExceptionError(ex));
}
}
}
public class ValidationResultError : ResultErrorBase
{
public ValidationResultError(string field, string message)
: base($"Validation failed for {field}: {message}")
{
}
}
public class UnauthorizedResultError : ResultErrorBase
{
public UnauthorizedResultError()
: base("User is not authorized to perform this action")
{
}
}
public class OrderService
{
private readonly IAuthService authService;
private readonly IOrderRepository orderRepository;
public OrderService(
IAuthService authService,
IOrderRepository orderRepository)
{
this.authService = authService;
this.orderRepository = orderRepository;
}
public Result<Order> CreateOrder(OrderRequest request)
{
if (!this.authService.CanCreateOrders())
{
return Result<Order>.Failure<UnauthorizedResultError>();
}
if (request.Quantity <= 0)
{
return Result<Order>.Failure()
.WithError(new ValidationResultError("Quantity", "Must be greater than zero"));
}
var order = this.orderRepository.Create(request);
return Result<Order>.Success(order, "Order created successfully");
}
}
public class DataService
{
private readonly IDataRepository repository;
private readonly ILogger<DataService> logger;
public DataService(
IDataRepository repository,
ILogger<DataService> logger)
{
this.repository = repository;
this.logger = logger;
}
public Result<Data> GetData()
{
try
{
var data = this.repository.FetchData();
return Result<Data>.Success(data);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Failed to fetch data");
return Result<Data>.Failure()
.WithError(new ExceptionError(ex))
.WithMessage("Failed to fetch data");
}
}
}
- Early Returns: Return failures as soon as possible to avoid unnecessary processing.
public class OrderProcessor
{
public Result<Order> ProcessOrder(OrderRequest request)
{
if (request == null)
{
return Result<Order>.Failure("Request cannot be null");
}
if (!request.IsValid)
{
return Result<Order>.Failure("Invalid request");
}
// Continue processing
return Result<Order>.Success(new Order(request));
}
}
- Meaningful Messages: Include context in error messages.
public Result ProcessOrderById(int orderId, string status)
{
return Result.Failure($"Failed to process order {orderId}: Invalid status {status}");
}
- Type-Safe Errors: Use strongly-typed errors for better error handling.
public class OrderNotFoundError : ResultErrorBase
{
public OrderNotFoundError(int orderId)
: base($"Order {orderId} not found")
{
}
}
public class UserRepository
{
private readonly DbContext dbContext;
public UserRepository(DbContext dbContext)
{
this.dbContext = dbContext;
}
public Result<User> GetById(int id)
{
try
{
var user = this.dbContext.Users.FindById(id);
if (user == null)
{
return Result<User>.Failure<NotFoundResultError>();
}
return Result<User>.Success(user);
}
catch (Exception ex)
{
return Result<User>.Failure()
.WithError(new ExceptionError(ex))
.WithMessage($"Failed to retrieve user {id}");
}
}
}
public class UserService
{
private readonly IUserRepository repository;
private readonly IValidator<User> validator;
private readonly IMapper mapper;
public UserService(
IUserRepository repository,
IValidator<User> validator,
IMapper mapper)
{
this.repository = repository;
this.validator = validator;
this.mapper = mapper;
}
public Result<UserDto> UpdateUser(int id, UpdateUserRequest request)
{
var getUserResult = this.repository.GetById(id);
if (getUserResult.IsFailure)
{
return Result<UserDto>.Failure()
.WithErrors(getUserResult.Errors)
.WithMessages(getUserResult.Messages);
}
var user = getUserResult.Value;
user.Update(request);
var validationResult = this.validator.Validate(user);
if (!validationResult.IsValid)
{
return Result<UserDto>.Failure()
.WithError(new ValidationResultError("User", validationResult.Error));
}
var savedUser = this.repository.Save(user);
return Result<UserDto>.Success(this.mapper.ToDto(savedUser));
}
}
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService productService;
public ProductsController(IProductService productService)
{
this.productService = productService;
}
[HttpGet]
public async Task<IActionResult> GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var result = await this.productService.GetProductsAsync(page, pageSize);
if (result.IsFailure)
{
return this.BadRequest(new
{
Errors = result.Errors.Select(e => e.Message),
Messages = result.Messages
});
}
return this.Ok(new
{
Data = result.Value,
Pagination = new
{
result.CurrentPage,
result.TotalPages,
result.TotalCount,
result.HasNextPage,
result.HasPreviousPage
}
});
}
}
public class OrderProcessor
{
private readonly IOrderRepository orderRepository;
private readonly IInventoryService inventoryService;
private readonly IPaymentService paymentService;
private readonly INotificationService notificationService;
private readonly IValidator<OrderRequest> validator;
private readonly ILogger<OrderProcessor> logger;
public OrderProcessor(
IOrderRepository orderRepository,
IInventoryService inventoryService,
IPaymentService paymentService,
INotificationService notificationService,
IValidator<OrderRequest> validator,
ILogger<OrderProcessor> logger)
{
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.notificationService = notificationService;
this.validator = validator;
this.logger = logger;
}
public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request)
{
// Validate request
var validationResult = await this.ValidateOrderRequestAsync(request);
if (validationResult.IsFailure)
{
return Result<Order>.Failure()
.WithErrors(validationResult.Errors)
.WithMessage("Order validation failed");
}
// Reserve inventory
var inventoryResult = await this.ReserveInventoryAsync(request.Items);
if (inventoryResult.IsFailure)
{
return Result<Order>.Failure()
.WithErrors(inventoryResult.Errors)
.WithMessage("Inventory reservation failed");
}
try
{
// Process payment
var paymentResult = await this.ProcessPaymentAsync(request.Payment);
if (paymentResult.IsFailure)
{
// Rollback inventory reservation
await this.ReleaseInventoryAsync(request.Items);
return Result<Order>.Failure()
.WithErrors(paymentResult.Errors)
.WithMessage("Payment processing failed");
}
// Create order
var order = await this.CreateOrderAsync(request, paymentResult.Value);
if (order.IsFailure)
{
// Rollback payment and inventory
await this.ReversePaymentAsync(paymentResult.Value);
await this.ReleaseInventoryAsync(request.Items);
return Result<Order>.Failure()
.WithErrors(order.Errors)
.WithMessage("Order creation failed");
}
// Send notifications
await this.notificationService.SendOrderConfirmationAsync(order.Value);
return Result<Order>.Success(order.Value)
.WithMessage("Order processed successfully");
}
catch (Exception ex)
{
this.logger.LogError(ex, "Unexpected error during order processing");
return Result<Order>.Failure()
.WithError(new ExceptionError(ex))
.WithMessage("An unexpected error occurred while processing the order");
}
}
private async Task<Result> ValidateOrderRequestAsync(OrderRequest request)
{
try
{
var validationResult = await this.validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
return Result.Failure()
.WithError(new ValidationResultError("Order", validationResult.Message));
}
return Result.Success();
}
catch (Exception ex)
{
this.logger.LogError(ex, "Validation error");
return Result.Failure()
.WithError(new ExceptionError(ex));
}
}
private async Task<Result<InventoryReservation>> ReserveInventoryAsync(IEnumerable<OrderItem> items)
{
try
{
return await this.inventoryService.ReserveItemsAsync(items);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Inventory reservation error");
return Result<InventoryReservation>.Failure()
.WithError(new ExceptionError(ex));
}
}
private async Task<Result<PaymentTransaction>> ProcessPaymentAsync(PaymentDetails payment)
{
try
{
var transaction = await this.paymentService.ProcessAsync(payment);
if (transaction.IsFailure)
{
this.logger.LogWarning("Payment failed: {Message}", transaction.Messages.FirstOrDefault());
}
return transaction;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Payment processing error");
return Result<PaymentTransaction>.Failure()
.WithError(new ExceptionError(ex));
}
}
private async Task<Result<Order>> CreateOrderAsync(OrderRequest request, PaymentTransaction transaction)
{
try
{
var order = new Order(request, transaction);
return await this.orderRepository.InsertResultAsync(order);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Order creation error");
return Result<Order>.Failure()
.WithError(new ExceptionError(ex));
}
}
private async Task ReleaseInventoryAsync(IEnumerable<OrderItem> items)
{
try
{
await this.inventoryService.ReleaseReservationAsync(items);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error releasing inventory");
}
}
private async Task ReversePaymentAsync(PaymentTransaction transaction)
{
try
{
await this.paymentService.ReverseTransactionAsync(transaction);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error reversing payment");
}
}
}
This example demonstrates:
- Proper Error Handling
- Each operation returns a
Result
orResult<T>
- Errors are properly propagated and transformed
- Rollback operations are performed when needed
- Clean Code Practices
- Follows the .editorconfig standards
- Proper use of dependency injection
- Clear separation of concerns
- Consistent error handling
- Workflow Management
- Sequential processing with proper validation
- Rollback mechanisms for failed operations
- Proper logging at each step
- Result Pattern Usage
- Consistent use of Result objects
- Proper error aggregation
- Clear success/failure paths
- Resource Management
- Proper cleanup in case of failures
- Structured error handling
- Comprehensive logging
The DevKit provides extension methods for repositories that don't natively support the Result pattern. These extensions wrap standard repository operations in Result objects, providing consistent error handling and operation results across your application.
- Read-Only Repository Extensions (
GenericReadOnlyRepositoryResultExtensions
):
- Count operations
- Find operations
- Paged query operations
- Repository Extensions (
GenericRepositoryResultExtensions
):
- Insert operations
- Update operations
- Upsert operations
- Delete operations
public class UserService
{
private readonly IGenericRepository<User> _repository;
// Insert with Result
public async Task<Result<User>> CreateUserAsync(User user)
{
return await _repository.InsertResultAsync(user);
}
// Update with Result
public async Task<Result<User>> UpdateUserAsync(User user)
{
return await _repository.UpdateResultAsync(user);
}
// Delete with Result
public async Task<Result<RepositoryActionResult>> DeleteUserAsync(int id)
{
return await _repository.DeleteByIdResultAsync(id);
}
}
public class ProductService
{
private readonly IGenericReadOnlyRepository<Product> _repository;
// Count with Result
public async Task<Result<long>> GetProductCountAsync()
{
return await _repository.CountResultAsync();
}
// Find One with Result
public async Task<Result<Product>> GetProductByIdAsync(int id)
{
return await _repository.FindOneResultAsync(id);
}
// Find All Paged with Result
public async Task<PagedResult<Product>> GetProductsPagedAsync(
int page = 1,
int pageSize = 10)
{
return await _repository.FindAllPagedResultAsync(
ordering: "Name ascending",
page: page,
pageSize: pageSize);
}
}
public class OrderService
{
private readonly IGenericReadOnlyRepository<Order> _repository;
// Complex query with specifications
public async Task<PagedResult<Order>> GetOrdersAsync(
FilterModel filterModel,
IEnumerable<ISpecification<Order>> additionalSpecs = null)
{
return await _repository.FindAllPagedResultAsync(
filterModel,
additionalSpecs);
}
// Query with includes
public async Task<Result<Order>> GetOrderWithDetailsAsync(int id)
{
return await _repository.FindOneResultAsync(
id,
options: new FindOptions<Order>
{
Include = new IncludeOption<Order>(o => o.OrderItems)
});
}
}
The extensions automatically handle exceptions and wrap them in Result objects:
public class InventoryService
{
private readonly IGenericRepository<Inventory> _repository;
public async Task<Result<Inventory>> UpdateInventoryAsync(Inventory inventory)
{
var result = await _repository.UpdateResultAsync(inventory);
if (result.IsFailure)
{
// Check for specific errors
if (result.HasError<ExceptionError>())
{
// Handle database exception
_logger.LogError(result.Errors.First().Message);
}
}
return result;
}
}
- Consistent Usage: Use these extensions throughout the application for consistent error handling:
// Instead of:
try {
var product = await _repository.FindOneAsync(id);
return product;
}
catch (Exception ex) {
// Handle error
}
// Use:
var result = await _repository.FindOneResultAsync(id);
return result;
- Combining Operations: Chain repository operations while maintaining error handling:
public async Task<Result<Order>> ProcessOrderAsync(Order order)
{
// Check inventory
var inventoryResult = await _inventoryRepository
.FindOneResultAsync(order.ProductId);
if (inventoryResult.IsFailure)
return Result<Order>.Failure()
.WithErrors(inventoryResult.Errors);
// Insert order
var orderResult = await _orderRepository
.InsertResultAsync(order);
return orderResult;
}
- Paged Queries with Specifications:
public async Task<PagedResult<Product>> SearchProductsAsync(
string searchTerm,
int page = 1,
int pageSize = 10)
{
var specification = new Specification<Product>(p => p.Name.Contains(searchTerm));
return await _repository.FindAllPagedResultAsync(specification);
}
- Filtering with model:
public async Task<PagedResult<Order>> GetOrdersAsync(FilterModel filterModel)
{
var specifications = new List<ISpecification<Order>>
{
new Specification<Order>(o => o.Status == OrderStatus.Active)
};
return await _repository.FindAllPagedResultAsync(filterModel, specifications);
}
These extensions provide a seamless way to integrate the Result pattern with existing repository implementations, ensuring consistent error handling and operation results across your application.
Composable, type-safe operations for elegant Result error handling and flow control.
The functional programming extensions transform the Result pattern from a simple success/failure
container into a powerful composition tool. By providing fluent, chainable operations like Map
,
Bind
, and Match
, complex workflows can be expressed as a series of small, focused
transformations. This approach eliminates nested error handling, reduces complexity, and makes the
code's intent clearer.
Each operation maintains the Result context, automatically handling error propagation and ensuring type safety throughout the chain. The pattern enables both synchronous and asynchronous operations, side effects, and validations while keeping the core business logic clean and maintainable. This functional style particularly shines in handling complex flows where multiple operations must be composed together, each potentially failing, with proper error context preserved throughout the chain.
Essential value transformations and validations ensuring Result integrity.
- Map: Transform success value into different type
await result.MapAsync(async (user, ct) => await LoadUserPreferencesAsync(user));
- Bind: Chain Results together, preserving errors
await result.BindAsync(async (user, ct) => await ValidateAndTransformUserAsync(user));
- Ensure: Verify condition on success value
await result.EnsureAsync(
async (user, ct) => await CheckUserStatusAsync(user),
new Error("Invalid status"));
- Try: Wrap async operation in Result handling exceptions
await Result<User>.TryAsync(async ct => await repository.GetUserAsync(userId, ct));
- Validate: Validate collection of values using FluentValidation
await result.ValidateAsync(new UserValidator(),
strategy => strategy.IncludeRuleSets("Create"));
Execute operations without changing Result value.
- Tap: Execute side effect on success value
await result.TapAsync(async (user, ct) => await _cache.StoreUserAsync(user));
- TeeMap: Transform and execute side effect
await result.TeeMapAsync(
user => user.ToDto(),
async (dto, ct) => await _cache.StoreDtoAsync(dto));
- Do: Execute action regardless of Result state
await result.DoAsync(async ct => await InitializeSystemAsync(ct));
- AndThen: Chain operations preserving original value
await result.AndThenAsync(async (user, ct) => await ValidateUserAsync(user));
Conditional logic and alternative paths.
- Filter: Convert to failure if predicate fails
await result.FilterAsync(
async (user, ct) => await HasValidSubscriptionAsync(user),
new SubscriptionError("Invalid subscription"));
- Unless: Convert to failure if predicate succeeds
await result.UnlessAsync(
async (user, ct) => await IsBlacklistedAsync(user),
new BlacklistError("Blacklisted"));
- OrElse: Provide fallback value on failure
await result.OrElseAsync(
async ct => await LoadDefaultUserAsync());
- Switch: Execute conditional side effect
await result.SwitchAsync(
user => user.IsAdmin,
async (user, ct) => await NotifyAdminLoginAsync(user));
Value mappings and collection handling.
- BiMap: Transform both success and failure cases
await result.BiMap(
user => new UserDto(user),
errors => errors.Select(e => new PublicError(e)));
- Choose: Filter optional values
await result.ChooseAsync(async (user, ct) =>
user.IsActive ? Option<UserDto>.Some(new UserDto(user)) : Option<UserDto>.None());
- Collect: Transform collection preserving all errors
await result.CollectAsync(async (user, ct) => await ValidateUserAsync(user));
Success/failure case handling.
- Match: Handle success/failure with async functions
await result.MatchAsync(
async (user, ct) => await ProcessUserAsync(user),
async (errors, ct) => await HandleErrorsAsync(errors));
- MatchAsync (mixed): Handle with mix of sync/async functions
await result.MatchAsync(
async (user, ct) => await ProcessUserAsync(user),
errors => "Processing failed");
- Match (values): Return different values for success/failure
await result.Match(
success: "Valid",
failure: "Invalid");
Clean validation, external integration and transformation flow with automatic error propagation.
The chain processes a list of persons through a series of validations and transformations.
Starting with input validation (age checks, location requirements), it transforms the data (email
normalization), interacts with external services (email notifications), and performs final
validations (database checks). Each operation in the chain either transforms the data or validates
it, with errors propagating automatically through the chain (ResultT
).
The functional style ensures that if any step fails, subsequent operations are skipped and the error context is preserved. The chain concludes by matching the result to either a success or failure message.
flowchart TB
Input[Result List<Person>] --> A
subgraph "Initial Validation"
A[Do - Log Start] --> B[Validate persons]
B --> C[Ensure age >= 18]
C --> E[Filter locations]
end
subgraph "Data Transformation"
E --> F[Map emails lowercase]
end
subgraph "External Operations"
F --> H[AndThenAsync send emails]
end
subgraph "Final Validation"
H --> K[EnsureAsync DB check]
end
K --> Final[Match result to string]
%% Side Effects
subgraph "Side Effects System"
direction LR
Email[Email Service]
DB[Database]
end
H -.-> Email
K -.-> DB
var people = new List<Person>{ personA, personB };
var result = await Result<List>.Success(people)
.Do(() => logger.LogInformation("Starting person processing"))
.Validate(validator)
.Ensure(
persons => persons.All(p => p.Age >= 18),
new Error("All persons must be adults"))
.Filter(
persons => persons.All(p => p.Locations.Any()),
new Error("All persons must have at least one location"))
.Map(persons => persons.Select(p => {
var email = EmailAddress.Create(p.Email.Value.ToLowerInvariant());
return new Person(p.FirstName, p.LastName, email, p.Age, p.Locations);
}).ToList())
.AndThenAsync(async (persons, ct) =>
await emailService.SendWelcomeEmailAsync(persons),
CancellationToken.None)
.EnsureAsync(async (persons, ct) =>
await database.PersonsExistAsync(persons),
new Error("Not all persons were saved correctly"))
.Match(
list => $"Successfully processed {list.Count} persons",
errors => $"Processing failed: {string.Join(", ", errors)}"
);