To use an Action
delegate here would translate to async void
which is a well-known anti-pattern (with exceptions).
See, the async
keyword is not part of the signature. It is an implementation detail, that allows the use of await
inside the method.
So, for ExecuteAction
it does not make a difference. All it sees is a void Action()
delegate. And using it like ExecuteAction( async () => { await SomethingAsync(); });
comes with all the pitfalls and downsides layed out in the article, I linked above. (tl;dr: really not recommended to do)
See also docs on the keyword itself:
void. async void methods are generally discouraged for code other than event handlers because callers cannot await those methods and must implement a different mechanism to report successful completion or error conditions.
- async - C# Reference
If you want to offer both sync and async experiences to your audience, you could do something like:
// your already known implementation
private void ExecuteAction(Action action)
{
// ...
}
/// <summary>Async Version</summary>
/// <example>
/// Usage:
/// <code>
/// await ExecuteAsync( () => { ... } );
/// </code>
/// or
/// <code>
/// await ExecuteAsync( async () => { ... await SomethingAsync(); ... } );
/// </code>
/// </example>
private async Task ExecuteAsync(Func<Task> asyncAction)
{
// Before client-defined function code goes here
await asyncAction();
// After client-defined function code goes here
}
If you need to, you can also pass a state object and avoid it being captured:
/// <example>
/// <code>
/// await ExecuteAsync( o => { ... }, someStateObject );
/// </code>
/// </example>
private async Task ExecuteAsync(Func<object,Task> asyncAction, object state = null)
{
await asyncAction(state);
}
Providing async API, I'd also always give the opportunity to pass a CancellationToken:
/// <example>
/// <code>
/// await ExecuteAsync( o => { ... }, someStateObject, cancel );
/// </code>
/// </example>
private async Task ExecuteAsync(Func<object, CancellationToken, Task> asyncAction, object state = null, CancellationToken token = default)
{
await asyncAction(state, token);
}
If this is GUI-related, you may also consider reporting progress:
/// <example>
/// <code>
/// await ExecuteAsync( o => { ... }, someStateObject, p => { ... },cancel );
/// </code>
/// </example>
private async Task ExecuteAsync<TProgress>(
Func<object, IProgress<TProgress>, CancellationToken, Task> asyncAction,
object state = null,
IProgress<TProgress> progress = null,
CancellationToken token = default)
{
await asyncAction(state, progress, token);
}
And of course you can make your state object type-safe
private async Task ExecuteAsync<TState, TProgress>(
Func<TState, IProgress<TProgress>, CancellationToken, Task> asyncAction,
TState state = null,
IProgress<TProgress> progress = null,
CancellationToken token = default)
{
await asyncAction(state, progress, token);
}
Sources:
Async/Await - Best Practices in Asynchronous Programming # Avoid Async Void - Stephen Cleary, March 2013, MSDN Magazine
async - C# Reference