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:
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 the 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 Task
s 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 be 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 led 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.