1

Lets suppose we have an action execution wrapper like this:

private void ExecuteAction(Action action)
{
    Console.WriteLine($"starting action ...");
    action();
    Console.WriteLine($"finished action ...");
}

Lets suppose we call it like this:

ExecuteAction( async () => { ... } ); // 1

or like this:

ExecuteAction( () => { ... } ); // 2

I wonder what is the difference between the two call and how to handle inside the ExecuteAction() method? Is there any difference?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Zoltan Hernyak
  • 989
  • 1
  • 14
  • 35
  • 1
    What do you mean by "how to handle"? Do you want to differentiate between the two? I don't think you can. – Sweeper May 23 '23 at 05:36
  • The difference is, that _async_ keyword makes the lambda function asynchronous. It allows you to use the _await_ keyword within that function. – Musti May 23 '23 at 05:40
  • 1
    That would translate to `async void` which (with exceptions) is a well- known anti pattern. You may prefer `Func` to make it awaitable. – Fildor May 23 '23 at 05:41
  • 1
    @Musti that's a common misconception. `async` doesn't make it asynchronous. It still could be executed synchronously. – Fildor May 23 '23 at 05:44
  • 1
    @Fildor that's true, should've better expressed it as "allows it to be executed async". – Musti May 23 '23 at 05:46
  • Looks to me like a duplicate of https://stackoverflow.com/questions/39191791/c-sharp-async-within-an-action – Musti May 23 '23 at 05:46
  • Thats nonsense... it says do not use Action anymore, Funct must use, because we do not know if the caller calls the method async () => way or () => way? – Zoltan Hernyak May 23 '23 at 05:52
  • Action is inherently not awaitable. That's all I'm saying. – Fildor May 23 '23 at 05:58
  • And that being said: nothing stops you to offer both. One for synchronous api, one for potentially asynchronous api. – Fildor May 23 '23 at 06:01
  • @Musti You weren't wrong, though, since even MSDN defines the language to be used in that context as : _"If you use this modifier on a method or expression, it's referred to as an async method."_ - which I find really confusing at times. – Fildor May 23 '23 at 07:10

1 Answers1

5

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

Fildor
  • 14,510
  • 4
  • 35
  • 67
  • Thank for the very detailed answer! But I lost somewhere... so you suggest not to write `async () => ` or you suggesting create to different `ExecuteAction()` implementations: one for a non-async actions, and one for the async ones? – Zoltan Hernyak May 23 '23 at 09:04
  • 2
    Exactly. You just have to name them differently, because they have different return types. But, yes: Have one thing for Sync and one thing for Async. – Fildor May 23 '23 at 13:31