An overview of what I'm doing: In a loop, I'm starting a new Task<string> and adding it to a List<Task<string>>. The issue is, after the string is returned, the task throws a System.Threading.Tasks.TaskCanceledException and I don't know why. Below is a trimmed version of what I'm doing
public async Task<string> GenerateXml(object item)
{
using (var dbContext = new DatabaseContext())
{
//...do some EF dbContext async calls here
//...generate the xml string and return it
return "my xml data";
}
}
var tasks = new List<Task<string>>();
My loop looks like:
foreach (var item in items)
{
tasks.Add(Task.Run(() => GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
//also tried:
tasks.Add(Task.Run(async () => await GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
//both generate the same exception
//after looking at my code, I was using the ContinueWith on the GenerateXml method call, which should still work, right?
//I moved the continue with to the `Task.Run` and still get the exception.
}
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException which contains the TaskCanceledException
When I step through the code, it hits the return "my xml data"; but the exception is thrown.
What I'm trying to avoid with ContinueWith is when I loop each task and get the results, it doesn't throw the same AggregateException that it threw with WaitAll.
Here is a working console app that throws... I know the issue is with the ContinueWith, but why?
class Program
{
static void Main(string[] args)
{
var program = new Program();
var tasks = new List<Task<string>>();
tasks.Add(Task.Run(() => program.GenerateXml().ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException
foreach (var task in tasks)
{
Console.WriteLine(task.Result);
}
Console.WriteLine("finished");
Console.ReadKey();
}
public async Task<string> GenerateXml()
{
System.Threading.Thread.Sleep(3000);
return "my xml data";
}
}
As answerer Avram hints at, you are getting the exception because your list contains not the tasks that are running the GenerateXml() method, but rather those that are the continuations of the tasks running that method.
Since those tasks get run only when GenerateXml() throws an exception, if any call to GenerateXml() succeeds, then at least one of those continuation tasks won't get run. Instead, it's completed by being cancelled (i.e. when its antecedent task completes successfully), and so the call to WaitAll() sees that cancellation and throws the aggregate exception.
IMHO, the best way to address this is to stick with the async/await pattern. I.e. rather than using ContinueWith() directly, write the code so that it's readable and expressive. In this case, I would write a wrapper async method to call the GenerateXml() method, catching any exception that occurs, and returning the "" value in that case.
Here's a modified version of your MCVE showing what I mean:
class Program
{
static void Main(string[] args)
{
var tasks = new List<Task<string>>();
tasks.Add(SafeGenerateXml());
Task.WaitAll(tasks.ToArray());
foreach (var task in tasks)
{
Console.WriteLine(task.Result);
}
Console.WriteLine("finished");
Console.ReadKey();
}
static async Task<string> SafeGenerateXml()
{
try
{
return await GenerateXml();
}
catch (Exception)
{
return "";
}
}
static async Task<string> GenerateXml()
{
await Task.Delay(3000);
return "my xml data";
}
}
IMHO this is much more in keeping with the new async idioms in C#, much less prone to failure, and is much easier to understand what exactly is going on (i.e. by avoiding ContinueWith() altogether, you don't even have the chance to get confused about which task(s) is(are) being waited on, as you obviously did in your original code).
You are running the second task.
.ContinueWith((t) task.
To run the correct one, you need to refactor the code.
Split the line like this:
Task<string> t1 = Task.Run(() => program.GenerateXml());
t1.ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted);
tasks.Add(t1);
You can refactor tasks like this: (for error handling)
tasks.Add(program.GenerateXml().ContinueWith(t => {return t.IsFaulted? "": t.Result; }));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With