Alexey Subbota
Alexey Subbota

Reputation: 972

How to do Task.ContinueWith<Task>(...)

I use HttpClient and I want to write something like that:

HttpClient client = ...;
Task<string> getContent()
{
    return client.GetAsync(...)
   .ContinueWith( t => t.Result.Content.ReadAsStringAsync() );
}

I know I can write

.ContinueWith( t => t.Result.Content.ReadAsStringAsync().Result);

but one of pool's threads will be blocked. I want continueWithTask like in the Google Task library.

How can I accomplish it?

UPDATE

And yes, I do need to use Tasks instead of async/await and I really know what I want.

UPDATE2

I revised my views and now I think that I was wrong in choosing the technology. If anyone doubts, here is a great example of good code.

Upvotes: 0

Views: 2060

Answers (3)

Stephen Cleary
Stephen Cleary

Reputation: 457472

And yes, I do need to use Tasks instead of async/await and I really know what I want.

I strongly recommend Marc's answer. I cannot think of a good reason not to use async/await unless you are stuck on .NET 4.0 (i.e., Windows XP). And that can't be the case because you're using HttpClient and Task.Run. So please bear in mind that this answer is purely instructional and not recommended for production.

ContinueWith calls can be "chained", kind-of similar to how Promise.then works in JavaScript, but the out-of-the-box C# chaining semantics are more awkward than JavaScript's.

For one thing, Task<Task<T>> types are not automatically unwrapped. There is an Unwrap method available. For another, the use of .Result - a TPL relic more naturally used with ContinueWith - will wrap exceptions in an AggregateException, which can cause an interesting kind of "cascade" where your inner exception can get buried deeply inside nested AggregateException instances. Hence the presence of AggregateException.Flatten to straighten out the mess after-the-fact. Oh, and you should always pass a TaskScheduler to ContinueWith.

Here's a first attempt, explicitly specifying the TaskScheduler, using Unwrap to unwrap nested tasks, and avoiding the nested exceptions by using GetAwaiter().GetResult() instead of Result:

Task<string> getContent()
{
  // I am so sorry, future coder, but I cannot use await here because <insert good reason>
  return Task.Run(()=> client.GetAsync(...))
      .ContinueWith(t => t.GetAwaiter().GetResult().Content.ReadAsStringAsync(), TaskScheduler.Default).Unwrap()
      .ContinueWith(t => t.GetAwaiter().GetResult(), TaskScheduler.Default);
}

If you do this a lot in your code, you may want to use something like .Then that encapsulates some of this logic. Oh, and be sure to write an apology in the comments; even if the future maintainer is yourself, that's just the polite thing to do with code like this. ;)

Upvotes: 3

usr
usr

Reputation: 171246

but one of pool's threads will be blocked

That's not the case because the ContinueWith delegate is only called once the "antecedent" task has completed. Result will not block and it's even synchronization free.

The second .Result (ReadAsStringAsync().Result) is blocking, though. So you have to turn that into another ContinueWith.

In general, a sequence of sequential IOs turns into a sequence of ContinueWith.

In general, await is preferable here but you indicated that this would not work for you.

Upvotes: 0

Marc Gravell
Marc Gravell

Reputation: 1064244

These days you should avoid ContinueWith, preferring async/await unless you have very very specific reasons; I suspect this will work:

async Task<string> getContent()
{
    var foo = await client.GetAsync(...); // possibly with .ConfigureAwait(false)
    return await foo.Content.ReadAsStringAsync(); // possibly with .ConfigureAwait(false)
}

Upvotes: 6

Related Questions