The real power of Flow is its plug-and-play design. Because a Flow is just a ‘recipe’, it can be enhanced with new behaviours without ever touching the original code.
For example, you can take an existing Flow and easily add resiliency policies like retries and timeouts:
// Assume GetUserFlow is defined elsewhere and you can't modify it.
var originalFlow = GetUserFlow(123);
// At the call-site, you can enrich it with new behaviours.
var resilientFlow = originalFlow
.WithRetry(3)
.WithTimeout(TimeSpan.FromSeconds(5))
.DoOnFailure(ex =>
_logger.LogError(ex, "Ultimately failed to get user"));
Flow is designed to handle complex, real-world scenarios cleanly. Here is the core logic for a Kafka consumer that processes a message by composing multiple failable steps, including a partial recovery for one of the steps.
var consumeFlow =
Flow.Succeed(message)
.Select(_adapters.AsOrderId)
.Chain(orderId =>
_orders.FindOrderFlow(orderId))
.Validate(
order => order is not null,
_ => new NotFoundException("Order not found"))
.Chain(order =>
_rates
.GetShippingRateFlow(order.ShipTo)
.Recover(ex =>
ex is HttpNotFoundException
? Flow.Succeed(ShippingRate.StandardFallback)
: Flow.Fail<ShippingRate>(ex))
.Select(rate =>
(order, rate)))
.Select(x =>
_adapters.AsDispatchMessage(x.order, x.rate))
.Chain(dispatchMessage =>
_producer.ProduceFlow(dispatchMessage));
This recipe shows how to use Flow’s resiliency and recovery operators to handle failures gracefully.
// --- Adding Resiliency ---
var resilient = initialFlow
.WithRetry(3)
.WithTimeout(TimeSpan.FromSeconds(5));
// --- Handling Failures ---
var recovered = initialFlow.Recover(ex => GetFallbackFlow(ex));
Now that you have a solid high-level understanding of Flow, you are ready to dive into the details of the specific operators.