Youâve mastered the core operators, but have you ever wondered how .WithRetry() works under the hood?
Welcome to the Behaviour system.
This is where you level up from being a user of Flow to a creator, forging your own reusable superpowers to extend Flow.
To get started, letâs clarify two key terms:
Select, Chain, Recover).Each operator is concerned with a single, specific part of the Flow.
Flow with a new capability.Behaviours are applied with operators that start with With (e.g., .WithRetry, .WithTimeout) to signify that you are creating a new Flow *with* an added superpower.
Note on Applicability
The built-in behaviours that alter execution -
.WithRetry()and.WithTimeout()- are only applicable to failable operations, such asFlow.Create()or.Chain(). Applying them to a pure transformation, like.Select(), is a logical no-op and will result in an equivalent flow being returned.However, the generic
.WithBehaviour()operator is different. It is designed to be a universal tool for applying any custom logic, including logging or auditing, to any part of a flow. Therefore, it can be applied to any operator, not just failable ones.On the Horizon:
WithRetryand Resource ManagementPlease note that the interaction between
.WithRetry()andFlow.WithResourceis currently being enhanced. To guarantee resource safety, the correct architectural behaviour is for each retry attempt to re-acquire the resource. This feature is on the roadmap and will be available in an upcoming release.
This entire system is designed for extensibility: The IBehaviour interface is your entry point for building any custom behaviour you can imagine, which you can then apply using the generic .WithBehaviour() operator.
You should create a custom behaviour when you have a stateful, cross-cutting concern that you want to apply to different Flows as a single, reusable unit.
Good examples include:
Letâs build a simple circuit breaker from scratch. The goal: create a behaviour that will âtripâ (stop executing a Flow) after 3 consecutive failures.
First, we need a simple class to hold the state of our circuit breaker. This object will be shared and managed by our application.
public class CircuitBreakerState
{
public int ConsecutiveFailures { get; private set; }
public bool IsTripped(int failureThreshold) => ConsecutiveFailures >= failureThreshold;
public void RecordFailure() => ConsecutiveFailures++;
public void RecordSuccess() => ConsecutiveFailures = 0;
}
Letâs implement the IBehaviour interface now.
public class CircuitBreakerBehaviour(CircuitBreakerState state, int failureThreshold = 3) : IBehaviour
{
// This gives our custom behaviour a unique name for diagnostics.
public string OperationType => "Flow.CircuitBreaker";
// This is where the magic happens.
public IFlow<T> Apply<T>(IFlow<T> originalFlow)
{
// 1. Check the state BEFORE doing anything.
if (state.IsTripped(failureThreshold))
{
// If the circuit is open, immediately return a failed Flow.
return Flow.Fail<T>(new Exception("Circuit breaker is open."));
}
// 2. If the circuit is closed, decorate the original Flow with our logic.
return originalFlow
.DoOnSuccess(_ => state.RecordSuccess()) // On success, reset the counter.
.DoOnFailure(_ => state.RecordFailure()); // On failure, increment it.
}
}
Now you can plug your custom behaviour into any Flow with the generic .WithBehaviour() operator.
// Create an instance of your behaviour, along with its state.
var circuitBreaker = new CircuitBreakerBehaviour(new CircuitBreakerState());
// Now, apply it to any flow.
var resilientFlow = GetUserFromFlakyApiFlow(123)
.WithBehaviour(circuitBreaker);
// When this flow is executed, the circuit breaker will do its job.
var outcome = await FlowEngine.ExecuteAsync(resilientFlow);
Thatâs it! You now know how to extend Flow with your own powerful, reusable behaviours.
From here, you have a few options:
Youâve seen how to build custom behaviours. To see built-in behaviours like WithRetry and WithTimeout used in a complete, runnable application, explore the example project: