5
votes

I have a group of commands that inherit from a base class. The base class has the following declaration:

public virtual async Task Execute(object parameter){}

Inheriting classes provide an override of this method and not all of them await tasks. In these cases the compiler generates a warning:

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Is it correct to explicitly provide a task complete return value?

public override async Task Execute(object parameter) {
    //code containing no await statements...
    await Task.CompletedTask;
}
2

2 Answers

10
votes

You should avoid the use of the async keyword inside overridden methods which do not await on anything, and instead just return Task.CompletedTask:

public override Task Execute(object parameter) {
    //code containing no await statements...
    return Task.CompletedTask; // or Task.FromResult(0) for older .NET versions
}

This is one of the (few) use cases for such "mocked" tasks, as you may understand by looking at this question about Task.FromResult. Those considerations are still valid for Task.CompletedTask.

6
votes

As a counterpoint, there is a difference in the way exceptions are handled.

If an exception is raised from the synchronous code in the non-async version, then that exception is raised directly to the caller (very unusual for methods with asynchronous signatures).

If an exception is raised from the synchronous code in the async version, then that exception is captured and placed on the returned task (which is the expected behavior for methods with asynchronous signatures).

So, if the method is called as such, the different implementations would have different exception semantics:

var task = Execute(parameter); // non-async exceptions raised here
...
await task; // async exceptions raised here

Most of the time, though, the method is called and immediately awaited, so those two semantics merge together:

await Execute(parameter); // both async and non-async exceptions raised here

It probably won't matter, but I believe it's an important distinction to be aware of.

If the exception semantics are important to you, then you do want to use async to generate a state machine around your synchronous code, and you can disable the warning in your code:

#pragma warning disable 1998 // use async keyword to capture exceptions
public override async Task Execute(object parameter) {
#pragma warning restore 1998
  //code containing no await statements...
}