Skip to main content

Functional Operations

Result<T> and Result support functional composition methods that enable railway-oriented programming — chaining operations where a failure on any step short-circuits the rest.

Railway Pattern

Input ──► [Validate] ──► [Transform] ──► [Save] ──► Output
│ │ │
Failure Failure Failure
└──────────────┴──────────────┘
Error Track

Map

Transform the value inside a successful result. On failure, propagates the error unchanged.

Result<string> orderId = Result<string>.Ok("order-42");

Result<int> orderNumber = orderId.Map(id => int.Parse(id.Split('-')[1]));
// Result<int>.Ok(42)

Result<string> failed = Result<string>.Fail("Not found", ErrorType.NotFound);
Result<int> stillFailed = failed.Map(id => int.Parse(id));
// Result<int>.Fail("Not found", ErrorType.NotFound) — error passes through

Bind

Chain operations that return Result<T>. Enables composition without nested if statements.

Result<OrderDto> result = await GetOrderAsync(orderId)
.Bind(order => ValidateOrder(order))
.Bind(order => EnrichWithCustomer(order));

// If any step returns a failure, the chain stops and the error propagates

MapAsync / BindAsync

Async versions for async transformations:

Result<string> result = await Result<Guid>.Ok(productId)
.MapAsync(async id =>
{
var product = await _repository.GetAsync(id);
return product.Name;
});
Result<OrderDto> result = await GetOrderAsync(orderId)
.BindAsync(async order => await EnrichOrderAsync(order));

Tap

Execute a side effect on success without changing the result. Useful for logging or publishing events.

Result<string> result = await _mediator.Send(new CreateOrderCommand(...));

result.Tap(orderId =>
{
_logger.LogInformation("Order created: {OrderId}", orderId);
// Result is passed through unchanged
});

OnFailure

Execute a side effect on failure. Useful for error logging.

Result<ProductDto> result = await _mediator.Send(new GetProductQuery(id));

result.OnFailure((error, errorType) =>
{
_logger.LogWarning("Failed to get product: {Error} ({ErrorType})", error, errorType);
});

Match

Exhaustive branching — provide a handler for both success and failure cases:

// Returns a value from either branch
string message = result.Match(
onSuccess: product => $"Found: {product.Name}",
onFailure: (error, errorType) => $"Error: {error}"
);

// Use in Minimal API
IResult httpResult = result.Match(
onSuccess: product => Results.Ok(product),
onFailure: (error, errorType) => errorType switch
{
ErrorType.NotFound => Results.NotFound(error),
ErrorType.Validation => Results.BadRequest(error),
_ => Results.Problem(error)
}
);

Chaining Example

public async Task<Result<OrderConfirmationDto>> PlaceOrder(PlaceOrderCommand command)
{
return await ValidateInventory(command)
.BindAsync(async _ => await ReserveStock(command))
.BindAsync(async _ => await ChargePayment(command))
.BindAsync(async _ => await CreateOrder(command))
.MapAsync(async orderId => await BuildConfirmation(orderId))
.Tap(confirmation =>
_logger.LogInformation("Order {Id} placed", confirmation.OrderId))
.OnFailure((error, type) =>
_logger.LogWarning("Order failed: {Error}", error));
}

Available on Result (void)

All methods are also available on non-generic Result:

Result result = await _mediator.Send(new DeleteProductCommand(id));

result
.Tap(() => _logger.LogInformation("Product deleted"))
.OnFailure((error, type) => _logger.LogWarning("Delete failed: {Error}", error));

string message = result.Match(
onSuccess: () => "Deleted successfully",
onFailure: (error, _) => $"Failed: {error}"
);