Tuesday, November 12, 2013

Async / await for .NET Framework 4 and Stack Trace

If you find yourself wanting to use the async / await language of .NET Framework 4.5 but still need to target Windows XP, what you need is called Microsoft Async Targeting Pack.

The last .NET Framework that is supported on Windows XP is version 4. To allow for the new async / await features in .NET Framework 4, Microsoft came out with an “extension” to the Framework called Microsoft Async Targeting Pack. It is basically a set of DLLs which you can get as a NuGet package.

There are two packages you can choose from. The original one is called Async Targeting Pack for Visual Studio 11, which was later superseded by Async for .NET Framework 4, Silverlight 4 and 5, and Windows Phone 7.5 and 8.

If you go with the new one, as I originally did with my applications, sooner or later you hit one problem. Whenever an exception is thrown in a an async code, you will not get a stack trace information you would expect. Consider the following example:

private async void Button1_Click(object sender, RoutedEventArgs e)
{
try
{
await DoSomethingAsync();
}

catch (Exception ex)
{
Debug.Write(ex.StackTrace);
}
}

public async Task DoSomethingAsync()
{
await TaskEx.Run(() =>
{
throw new ApplicationException("My Application Exception");
});
}

The code is relatively straightforward. You click a button which runs an async method. This method throws an exception in its body. Once the exception propagates back to the click event handler, you catch it and write the stack trace information in the debug output.

If you run the code with Async for .NET Framework 4, Silverlight 4 and 5, and Windows Phone 7.5 and 8, you get the following output:

at Microsoft.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Microsoft.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task)
at Microsoft.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at Microsoft.Runtime.CompilerServices.TaskAwaiter.GetResult()
at WpfApplication1.MainWindow.<Button1_Click>d__0.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 34

The only thing you get to know is that the error was caught at line 34. There is no information about the real exception source at line 47. As you can imagine, this can easily make the debugging of your applications a nightmare.

Searching the web, I found some explanations as to why this is happening, what stack trace is and is not supposed to show, etc. I even found a few workarounds (a custom code you need to write in order to get the information you would expect), but that was not a way I wanted to go.

Later I found a much simpler solution. It turns out that if you run the code with the “earlier” or “older” version of the async targeting pack, the Async Targeting Pack for Visual Studio 11, the same code produces much friendlier stack trace:

Server stack trace: 
at WpfApplication1.MainWindow.<DoSomethingAsync>b__3() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 47
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()

Exception rethrown at [0]:
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at WpfApplication1.MainWindow.<DoSomethingAsync>d__5.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 45

Exception rethrown at [1]:
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at WpfApplication1.MainWindow.<Button1_Click>d__0.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 34

Now, that is the way to go!

There is one more gotcha, however. If you’re trying to install the Async Targeting Pack for Visual Studio 11 via Manage NuGet Packages tool in Visual Studio, you will not be able to find the package. This has been done by Microsoft so that people will always install the newer version if the are getting the package for the fist time. The older version of the package however is still available. To get it, you need to use the Package Manager Console and enter the following command:

PM> Install-Package Microsoft.CompilerServices.AsyncTargetingPack -Version 1.0.1

And that should take care of the stack trace problem!

For completeness, if you run our example code under .NET Framework 4.5, which has a native support for async / await, you get a similar output, telling you all you need to know about the exception:

    at WpfApplication1.MainWindow.<DoSomethingAsync>b__3() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 47
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at WpfApplication1.MainWindow.<DoSomethingAsync>d__5.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 45
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at WpfApplication1.MainWindow.<Button1_Click>d__0.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 34

That is - the good new is that it has been fixed in .NET Framework 4.5.

Monday, November 11, 2013

Accessing awaited decimal array’s item leads to Common Language Runtime detected an invalid program

While converting one of our products to use the async mechanism I discovered a strange .NET Framework behavior.

Suppose you have a following code in your command button:

decimal xyz = GetData()[0];

Let’s say the GetData implementation happens to be resource intensive which causes your UI to be unresponsive when the button is clicked. To fix this, you will convert the method into an awaitable one, such as:

public async Task<decimal[]> GetDataAsync()
{
return await Task.Run(() =>
{
// This would be something that takes a long time to return
return new decimal[] { 1, 2, 3 };
});
}

Finally, you will change your invocation code in the command button to:

decimal xyz = (await GetDataAsync())[0];

Once you compile and run the code, it will produce System.InvalidProgramException exception stating that Common Language Runtime detected an invalid program.

After some investigation I found out that the issue occurs only if ALL of these conditions are met:

  • You are using an array
  • The array is type of decimal
  • You want to await the result and get an item of the array in a one-liner

That being said, if you modify the example above into using an array of int for example, the exception will not be raised and everything will work as expected.

Similarly, if you split the invocation code in two lines, the error will go away as well:

decimal[] abc = await GetDataAsync();
decimal xyz = abc[0];

Even though I agree that this code is much clearer than the original one-liner, the reason why the original code fails to compile remains a mystery to me. A possible bug in the .NET Framework?

Additionally, further testing revealed that this issue is present in .NET Framework 4.5, 4.5.1 as well as in the Async Targeting Pack for .NET Framework 4.0.

The code won’t fail on compilation time, but only on run time, when the async code is compiled on-demand, so watch out for this one!