Take for example the following interface:
interface IOracle
{
Task<string> GetAnswerAsync(string question);
}
Some implementations of this interface might use async/await. Others might not need to. For example, consider this simple toy implementation.
class SimpleOracle
{
public Dictionary<string, string> Lookup { get; set; }
// Warning CS1998: This async method lacks 'await' operators
// and will run synchonously.
public async Task<string> GetAnswerAsync(string question)
{
string answer = Lookup[question];
return answer;
}
}
The compiler warning CS1998 of course makes sense. The usual suggestion is to remove the async keyword and use Task.FromResult, but it misses a subtle issue. What if the code throws an exception? Then that code transform changes the behavior of the method: the async version will wrap any exception in a Task; the non-async version will not, without an explict try-catch.
Leaving the async keyword works exactly as I want, but it produces a compiler warning, and I don't think suppressing those is wise.
How should I refactor my method implementation to not produce a compiler warning while also wrapping all exceptions with Task as any other async method would?
The mechanical translation I use to convert from the async version that yields compiler warning CS1998 to a non-async version that behaves identically is as follows.
async keyword.try-catch.TaskCompletionSource<T> called tcs before the try-catch.return <expr>; with tcs.SetResult(<expr>); followed by return tcs.Task;.catch block to call tcs.SetException(e) followed by return tcs.Task;.For example:
public Task<string> GetAnswerAsync(string question)
{
var tcs = new TaskCompletionSource<string>();
try
{
string answer = Lookup[question];
tcs.SetResult(answer);
return tcs.Task;
}
catch (Exception e)
{
tcs.SetException(e);
return tcs.Task;
}
}
This can be expressed more generally by the following, although I don't know if it would be appropriate to actually introduce such a helper method into a codebase.
public static Task<T> AsyncPattern(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
return tcs.Task;
}
If you're using .NET 4.6, you can use Task.FromException to handle the exception case, just as you use FromResult to handle the successful case:
public Task<string> GetAnswerAsync(string question)
{
try
{
return Task.FromResult(Lookup[question]);
}
catch(Exception e)
{
return Task.FromException<string>(e);
}
}
If you're using .NET 4.5 then you'll need to write your own FromException method, but it's pretty trivial:
public static Task<T> FromException<T>(Exception e)
{
var tcs = new TaskCompletionSource<T>();
tcs.SetException(e);
return tcs.Task;
}
public static Task FromException(Exception e)
{
var tcs = new TaskCompletionSource<bool>();
tcs.SetException(e);
return tcs.Task;
}