The history of .NET versioning is a complicated one, even when excluding the recent developments regarding .NET Core. Let’s start off by clarifying which components we are talking about: The .NET runtime (CLR), the base libraries (BCL) and the C# language (and VB.NET) are all versioned independently but released together as the .NET Framework, usually in combination with a new Visual Studio release.
.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 |
When you select which version of the .NET Framework you wish to “target” in a Visual Studio project, you are effectively setting the version of the CLR and BCL but not the compiler. This means you can use a newer Visual Studio release to compile code that will run on older .NET Framework versions while still using new C#/VB language features. For example, with the C# 3.0 compiler distributed with Visual Studio 2008, you can use auto-properties (compiler-generated backing fields for properties) while producing output that will run on .NET Framework 2.0.
Many interesting features require support in both the compiler and the BCL. Let’s look at Language Integrated Queries (LINQ), introduced in the .NET Framework 3.5. LINQ adds the ability use functional-style programming and to translate C#/VB expressions to query languages like SQL. This is enabled by a number of other prerequisite features: lambda expressions, extension methods, the query syntax and expression trees.
Lambda expressions are basically just a more concise syntax for anonymous methods. Instead of
myList.ForEach(delegate(string x) { return Console.WriteLine(x); });
you can now write:
myList.ForEach(x => Console.WriteLine(x));
This is a compiler feature introduced in C# 3.0 and requires no changes to the CLR or BCL. Note that the lambda-version of the code sample did not need to explicitly specify string
due to type inference. LINQ uses this to make long method chains with functional-style transformations much more readable.
Extension methods allow you to define new methods that appear to be part of existing types without having to modify them. You define a regular static
method in a static
class and annotate the method’s first argument with the this
keyword, like this:
static class MyExtensions
{
public static int CountOccurencesOfLetter(this string value, char letter)
{
return value.Split(letter).Length - 1;
}
}
Any instances of the type of this first argument now appear to have a method with the remaining arguments as an instance method:
string word = "banana";
Console.WriteLine(word.CountOccurencesOfLetter('a'));
Calls to extension methods are compiled to regular static
method invocations by the C# 3.0 compiler, making this a form of syntactic sugar. A new attribute called ExtensionAttribute
was added to the BCL, which is used by the compiler to mark and detect extension methods in compiled code. So if you were to decompile the code defined above, the result would look something like this:
static class MyExtensions
{
[Extension]
public static int CountOccurencesOfLetter(string value, char letter)
{
return value.Split(letter).Length - 1;
}
}
string word = "banana";
Console.WriteLine(MyExtensions.CountOccurencesOfLetter(word, 'a'));
LINQ uses extension methods to add a whole host of methods to the IEnumerable<T>
interface without requiring each implementation of the interface to provide these methods itself.
The query syntax allows you to replace method chains like this:
var result = list.Where(x => x.Id > 2).Select(x => x.Name);
with a more SQL-like syntax:
var result = from x in list where x.Id > 2 select x.Name;
This C# 3.0 feature works as long as list
provides the appropriate method signatures for Where()
, Select()
, etc. (directly or via extension methods). It does not depend on any specific types in the BCL.
Expression trees are the final ingredient for LINQ. When a lamda expression is assigned to a variable or method argument of the type Expression<T>
the compiler generates a tree-like data structure representing what the code would do rather than compiling it to actual IL code. LINQ uses expression trees to convert method calls on IQueryable<T>
to SQL queries rather than directly executing the specified lambdas like with IEnumerable<T>
.
Now that we have seen what LINQ can do for us, we don’t want to miss out on it. But for deployment tools like Zero Install, and especially its Bootstrapper, it is important to have the EXE run out-of-the-box on as many machines as possible. This means it is still advantageous to target .NET Framework versions as old as 2.0.
Fortunately, we can have our cake and eat it. The fantastic LinqBridge project backports most of LINQ to .NET 2.0. It does this by taking advantage of the separate versioning of the C#/VB compilers and the BCL we mentioned in the beginning of this article. When using features that depend on BCL classes, the compiler does not look in specific assemblies. Instead, it looks for specific namespaces in all referenced assemblies. The LinqBridge
library provides alternate implementations of things like the ExtensionAttribute
and the IEnumerable<T>
extension methods. By targeting the .NET Framework 2.0 BCL and referencing LinqBridge
with a C# 3.0 compiler, we get LINQ support without depending on the .NET Framework 3.5 on the target machine. LinqBridge
does not provide support for expression trees. Since Zero Install does not use any databases, this is still a perfect fit for us.
In the next part, we’ll look at .NET 4.x features and targeting multiple .NET versions.