I'm writing client libraries for Google Cloud APIs which have a fairly common pattern for async helper overloads:
- Do some brief synchronous work to set up a request
- Make an asynchronous request
- Transform the result in a simple way
Currently we're using async methods for that, but:
- Transforming the result of await ends up being annoying in terms of precedence - we end up needing
(await foo.Bar().ConfigureAwait(false)).TransformToBaz()
and the brackets are annoying. Using two statements improves readability, but means we can't use an expression-bodied method. - We occasionally forget
ConfigureAwait(false)
- this is solvable with tooling to some extent, but it's still a bit of a smell
Task<TResult>.ContinueWith
sounds like a good idea, but I've read Stephen Cleary's blog post recommending against it, and the reasons seem sound. We're considering adding an extension method for Task<T>
like this:
Potential extension method
public static async Task<TResult> Convert<TSource, TResult>(
this Task<TSource> task, Func<TSource, TResult> projection)
{
var result = await task.ConfigureAwait(false);
return projection(result);
}
We can then call this from a synchronous method really simply, e.g.
public async Task<Bar> BarAsync()
{
var fooRequest = BuildFooRequest();
return FooAsync(fooRequest).Convert(foo => new Bar(foo));
}
or even:
public Task<Bar> BarAsync() =>
FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo));
It seems so simple and useful that I'm slightly surprised there isn't something already available.
As an example of where I'd use this to make an expression-bodied method work, in the Google.Cloud.Translation.V2
code I have two methods to translate plain text: one takes a single string and one takes multiple strings. The three options for the single-string version are (simplified somewhat in terms of parameters):
Regular async method
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage)
{
GaxPreconditions.CheckNotNull(text, nameof(text));
var results = await TranslateTextAsync(new[] { text }, targetLanguage).ConfigureAwait(false);
return results[0];
}
Expression-bodied async method
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
(await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.ConfigureAwait(false))[0];
Expression-bodied sync method using Convert
public Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.Convert(results => results[0]);
I personally prefer the last of these.
I'm aware that this changes the timing of the validation - in the final example, passing a null
value for text
will immediately throw an ArgumentNullException
whereas passing a null
value for targetLanguage
will return a faulted task (because TranslateTextAsync
will fail asynchronously). That's a difference I'm willing to accept.
Are there differences in scheduling or performance that I should be aware of? (We're still constructing two state machines, because the Convert
method will create one. Using Task.ContineWith
would avoid that, but has all the problems mentioned in the blog post. The Convert
method could potentially be changed to use ContinueWith
carefully.)
(I'm somewhat tempted to post this on CodeReview, but I suspect the information in the answers will be more generally useful beyond whether this is specifically a good idea. If others disagree, I'm happy to move it.)