This repository demonstrates a small but practical technique for unifying execution pipelines that need to handle both:
- methods that return a value
(TResult)/Func<TResult> - methods that do not return a value (
void/Action/Action<T>)
In real systems (retry policies, logging wrappers, metrics, orchestration layers), you often want a single execution pipeline.
However, you quickly run into a mismatch:
Func<TResult>fits nicely into a composable pipelineActiondoes not
This typically leads to:
- duplicated logic
- separate code paths
- subtle divergence over time
Instead of branching the pipeline, adapt the void case.
Convert:
Action
into:
Func<Unit>
by wrapping the call and returning a placeholder value. This allows everything to flow through a single generic path.
public static class Pipeline
{
public static TResult Execute<TResult>(Func<TResult> func)
{
return func();
}
public static void Execute(Action action)
{
Execute(() =>
{
action();
return Unit.Value;
});
}
}
public readonly struct Unit
{
public static readonly Unit Value = new();
}This pattern enables:
- a single execution pipeline
- consistent behavior across all call types
- easier composition (retry, logging, metrics, etc.)
- reduced duplication
The value is not the Unit type itself, but the ability to treat all operations uniformly.
- retry mechanisms
- resilience policies
- middleware-style pipelines
- decorators / cross-cutting concerns
- async orchestration layers
- This is a simple adapter, not a full functional programming abstraction.
- The naming
Unitis conventional, but any placeholder type works. - In async scenarios, the same idea applies to:
Func<Task>Func<Task<TResult>>
If your pipeline is generic, your inputs should be too.
Adapting void into a value is often the simplest way to get there.