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
async voidwhich (with exceptions) is a well- known anti pattern. You may preferFunc<Task>to make it awaitable.asyncdoesn't make it asynchronous. It still could be executed synchronously.