.NET backward compatibility – Part 2

In the previous part we talked about how different parts of the .NET Framework are versioned and how we can backport parts of it.

Different versions of the .NET Framework are considered in-place upgrades of each other when they share the same CLR. Versions based on different CLRs can be installed side-by-side. Let’s look at the table from the last post again:

.NET Framework Visual Studio Included with Windows CLR BCL C# Major new feature
1.0 2002 1.0 1.0 1.0
1.1 2003 Server 2003 1.1 1.1 1.2
2.0 2005 2.0 2.0 2.0 Generics
3.0 Vista 2.0 3.0 2.0 WPF
3.5 2008 7 2.0 3.5 3.0 LINQ
4.0 2010 4.0 4.0 4.0 dynamic keyword, optional parameters
4.5 2012 8 4.0 4.5 5.0 async keyword
4.5.1 2013 8.1 4.0 4.5.1 5.0
4.6 2015 10 4.0 4.6 6.0 Null-conditional operator

So .NET Framework 3.5 replaces 2.0 and 3.0 while 4.6 replaces 4.0, 4.5 and 4.5.1. The post Introduction to .NET Framework Compatibility on the .NET Blog provides some great insight into how the .NET Framework ensures application compatibility when such upgrades are performed. Versions 1.0 and 1.1 of the .NET Framework are unsupported on current Windows versions and have become pretty much obsolete.

Looking at the table one more time you can see that Windows 8 is the first release to bundle a .NET Framework version that uses the CLR 4.0. A side-by-side installation of the .NET Framework 3.5 is available as an optional component but not installed by default. When you try to run a .NET 2.0/3.x application on a fresh installation of Windows 8, 8.1 or 10 you are presented with a dialog like this:

.NET Framework 3.5 as an optional Windows component

Windows Vista and 7 on the other hand bundle .NET Framework versions with the CLR 2.0. So how do we create a single .NET executable that “just works” on all popular Windows versions without requiring the user to download additional components? By adding this to the project’s App.config:

<startup>
  <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
  <supportedRuntime version="v2.0.50727" />
</startup>

When compiling a project Visual Studio places the App.config file next to the generated executable and names it like MyApp.exe.config. When launching a .NET executable the system looks for such .config files before executing any of the EXE’s actual code. The <supportedRuntime> tags tell the system which CLRs the application may be run in, regardless of the .NET Framework version it targeted at build time. This way we get the best of both worlds: Our executable runs on the CLR 4.0 when available but also works on the CLR 2.0. However, it is now our responsibility to make sure we do not depend on any behaviors or quirks that changed between the two CLR versions.

Now, let’s take a look at the async keyword introduced in the .NET Framework 4.5. This feature simplifies writing callback-based asynchronous code. Here’s an example:

private async Task WaitAsync()
{
    var client = new HttpClient();
    Task<string> task = client.GetStringAsync("http://0install.de/");
    string result = await task;
    MessageBox.Show(result);
}

The async keyword in the method signature tells to compiler to expect await calls in the method body. These mark points where the sequential execution of a method ends and the following code is transformed into a callback. Such callbacks are used to resume the execution when a blocking operation represented by a Task object (e.g. an HTTP request) has been completed. Notice the return type of the WaitAsync() method itself is also Task even though it has no return statements. The compiler implicitly returns Tasks at await points, allowing consumers of your method to again use await.

The code we wrote above is transformed by the compiler into something like this:

private Task WaitAsync()
{
    var client = new HttpClient();
    Task<string> task = client.GetStringAsync("http://0install.de/");
    task.ContinueWith(t =>
    {
        string result = t.Result;
        MessageBox.Show(result);
    });
}

Asynchronous methods often provide an overload with an argument of the type CancellationToken. These tokens can used to request cooperative cancellation. This means that the asynchronous method checks for pending cancellation requests at fixed points and has the opportunity to clean up after itself. Another common argument type for asynchronous methods is the
IProgress<T> interface. Callers can use this to provide a callback for tracking the progress of long-running operations.

As you can see, the async feature, much like LINQ, depends on specific classes and methods being present in the BCL (or any other library) while leaving most of the “magic” to the compiler. Microsoft provides a NuGet package called Microsoft.Bcl.Async that backports the Async-related BCL classes introduced in the .NET Framework 4.5 to the .NET Framework 4.0. This is similar to the LinqBridge library we talked about in the previous post.

We started developing Zero Install for Windows in 2010, 2 years before .NET Framework 4.5 was released in 2012. This lead us to implement our own system for managing asynchronous tasks based on classic threads rather than callbacks. We extracted this functionality from Zero Install to a shared utils library called NanoByte.Common. We’ll talk about this in more detail in a future post.

After the release of the .NET Framework 4.5 we refactored the cancellation- and progress-related classes of our task code (not the actual task execution though) to match the signatures of the equivalent .NET classes as closely as possible. Unfortunately, we could not simply use the backported classes from the NuGet package mentioned above. Unlike their Microsoft-provided equivalents our classes are serializable, making them compatible with .NET Remoting. While this feature is considered legacy and has been mostly superseded by WCF it is still very useful for IPC between applications and services running on the same machine. In Zero Install we use it for communicating with the Store Service. For easier interoperability we added extension methods and implicit type casts that convert between our custom classes and their .NET Framework counterparts when NanoByte.Common is compiled for .NET 4.5 or later.

In the next part we’ll look at library versioning in .NET, before talking about targeting multiple .NET Framework versions in the last part.

Posted in Blog, CodeProject

Leave a Reply

Your email address will not be published. Required fields are marked *

*