Ready to cook up something more advanced? This document provides practical recipes for specific scenarios you might encounter.
Problem: You have several long-running operations and you want to run them all in parallel and collect the results.
Solution: Use Flow.All.
Task.WhenAll for Flows.Flows concurrently and, if they all succeed, returns a Flow containing an array of their results.Flow fails, the entire operation fails immediately.var allUsersFlow = Flow.All(
GetUserAsync(1),
GetUserAsync(2),
GetUserAsync(3)
);
Problem: You have multiple sources for the same data (e.g., a cache and a database), and you want the result from whichever one finishes first.
Solution: Use Flow.Any.
Task.WhenAny, but it specifically waits for the first Flow to succeed.Flow.Any will cancel all losing branches as soon as the first success arrives â provided those branches are built from cancellable operations.Flow.Create((ct) => Task<T>) and cancellable operator overloads to enable prompt coâoperative cancellation.// Prefer cancellable flows so losing branches stop quickly.
var fastestUserFlow = Flow.Any(
Flow.Create<User>(async ct => await GetUserFromCacheAsync(1, ct)),
Flow.Create<User>(async ct => await GetUserFromDbAsync(1, ct))
); // losers observe cancellation as soon as the winner succeeds
Note: The following behaviours (
.With...) are powered byFlowâs Behaviour system. You can learn more in Behaviours.
Problem: An operation in your Flow might fail intermittently due to a flaky network or a temporary service outage.
Solution: Use the .WithRetry() behaviour to automatically retry a failed operation.
var resilientFlow = CreateSometimesFailingFlow()
.WithRetry(3); // Tries up to 3 times before giving up
Problem: An operation in your Flow might hang indefinitely, tying up resources.
Solution: Use the .WithTimeout() behaviour to enforce a deadline.
var timelyFlow = CreateLongRunningFlow()
.WithTimeout(TimeSpan.FromSeconds(5)); // Gives up if it takes too long
Problem:
You want to build a truly robust Flow.
It should handle transient failures and unexpected hangs.
But it must still provide a fallback value if all else fails.
Solution:
Combine .WithRetry() and .WithTimeout() with .Recover().
This creates a powerful, multi-layered resiliency strategy.
Flow will first attempt the operation.
Then, it will retry on failure.
Finally, it will recover if all retries fail or a timeout occurs.
var superResilientFlow = CreateFlakyAndSlowFlow()
.WithTimeout(TimeSpan.FromSeconds(10)) // 1. Enforce a 10-second deadline.
.WithRetry(3) // 2. Retry up to 3 times on failure.
.DoOnFailure(ex => _logger.LogError(ex, "The operation ultimately failed."))
.Recover(ex => GetDefaultValue()); // 3. If all else fails, recover.
[!NOTE]
Execution Order Matters:
The order in which you apply these behaviours is crucial.
In the example above, the timeout wraps the entire retry logic.
This means the 10-second limit applies to the total time for all attempts.
If you applied
.WithRetry()first, each attempt would get its own timeout.
Problem: You want to race multiple sources, return the first success, and ensure the others stop immediately to save resources.
Solution: Use Flow.Any with cancellable operations.
var userId = 1;
// Build cancellable flows so the race can cancel losers.
var fromCache = Flow.Create<User>(async ct => await GetUserFromCacheAsync(userId, ct));
var fromDb = Flow.Create<User>(async ct => await GetUserFromDbAsync(userId, ct));
var firstWins = Flow.Any(fromCache, fromDb)
.DoOnSuccess(u => _logger.LogInformation($"Winner: {u.Id}"));
var outcome = await FlowEngine.ExecuteAsync(firstWins);
IDisposablesProblem: You need to use a resource that requires safe disposal, like an HttpClient, in the middle of a complex Flow.
Solution: Use Flow.WithResource.
using block, but in a way that composes beautifully with the rest of your Flow.Hereâs how you would use it to make an API call:
var userProfileFlow = Flow.Succeed("user-123")
.Chain(userId =>
Flow.WithResource(
acquire: () => new HttpClient(),
use: httpClient =>
// This is a new Flow that only runs
// within the scope of the HttpClient.
Flow.Create(async () => await httpClient.GetAsync($"/users/{userId}"))
.Chain(response => ProcessHttpResponseFlow(response))
)
);
A Note for the Curious: This pattern is often used inside a
.Chain()because the operation that needs the resource (like fetching a user profile) usually depends on the output of a previous step (like auserId). Since the entire âacquire-use-disposeâ block is a single, failable unit of work, it fits perfectly within.Chain(), which is designed for sequencing failable operations.
Now that youâve seen some practical recipes, you can dive deeper into the concepts that power them.
Youâve seen individual recipes. To see how they all come together in a complete, runnable application, explore the example project: