Transforming the result of await ends up being annoying in terms of precedence
I generally prefer to introduce a local var, but as you noted, that prevents expression-bodied methods.
We occasionally forget
ConfigureAwait(false)– this is solvable with tooling to some extent
Since you’re working on a library and should use ConfigureAwait(false)
everywhere, it may be worthwhile to use a code analyzer that enforces
ConfigureAwait usage. There’s a ReSharper plugin and a VS plugin that do this. I haven’t tried them myself, though.
Task<TResult>.ContinueWithsounds like a good idea, but I’ve read Stephen Cleary’s blog post recommending against it, and the reasons seem sound.
If you used ContinueWith, you’d have to explicitly specify
TaskScheduler.Default (this is the ContinueWith equivalent of
ConfigureAwait(false)), and also consider adding flags such as
DenyChildAttach. IMO it’s harder to remember how to use ContinueWith
correctly than it is to remember ConfigureAwait(false).
On the other hand, while ContinueWith is a low-level, dangerous method, if you use it correctly then it can give you minor performance improvements. In particular, using the state parameter can save you a delegate allocation. This is the approach commonly taken by the TPL and other Microsoft libraries, but IMO it lowers the maintainability too much for most libraries.
It seems so simple and useful that I’m slightly surprised there isn’t something already available.
The Convert method you suggest has existed informally as Then. Stephen doesn’t say so, but I assume that the name Then is from the
JavaScript world, where promises are the task equivalent (they are
both Futures).
On a side note, Stephen’s blog post takes this concept to an interesting
conclusion. Convert/Then is the bind for the Future monad, so it can
be used to implement LINQ-over-futures. Stephen Toub has also
published code for this (rather dated at this point, but interesting).
I have thought a few times about adding Then to my AsyncEx library,
but each time it didn’t make the cut because it’s pretty much the same
as just await. Its only benefit is solving the precedence problem by allowing method chaining. I assume it doesn’t exist in the framework for the
same reason.
That said, there’s certainly nothing wrong with implementing your own
Convert method. Doing so will avoid the parenthesis / extra local
variable and allow for expression-bodied methods.
I’m aware that this changes the timing of the validation
This is one of the reasons that I’m wary of eliding async/await (my blog post goes into more reasons).
In this case, I think it’s fine either way, since the “brief synchronous work to set up a request” is a preconditions check, and IMO it doesn’t matter where boneheaded exceptions are thrown (because they shouldn’t be caught anyway).
If the “brief synchronous work” was more complex – if it was something that could throw, or could reasonably throw after someone refactors it a year from now – then I would use async/await. You could still use Convert to avoid the precedence issue:
public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
.Convert(results => results[0])
.ConfigureAwait(false);