As .NET developers we’re used to running the latest version of the operating system on our computers and having the latest updates installed. However, that’s not always the case in enterprise environments where IT personnel are often a lot more conservative. They make sure that all critical business software will keep running without issues, before doing any large-scale updates.
As a result, the software we write can’t always take advantage of the latest .NET framework features.
Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering C#, Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.
In this article, we’ll take a look at the major differences between the .NET framework versions and explore how we can still target older versions of .NET framework even when using the latest development tools.
Editorial Note: If you are looking to create cross-platform apps, supporting Windows, macOS and Linux, and that can be used on the web, mobile, cloud, and embedded/IoT scenarios, make sure to read What’s New for .NET Developers and the Future of .NET.
The Evolution of .NET Framework
As of this writing, it’s been over fifteen years since the original release of .NET framework 1.0 and looking at it today, it seems much less impressive than it did at that time. Most of the features we are casually used to, were not even available then and we would have a hard time if we wanted to use it for development of modern applications today.
Here’s a quick rundown of the major features in every .NET release.
Table 1: .NET framework version history
.NET framework 1.0 included a very limited set of application frameworks. Only the following types of applications were supported:
- Console and Windows Forms applications for the desktop,
- Windows Services,
- ASP.NET Web Forms based web applications, and
- Web services using the SOAP protocol.
.NET framework 1.1 didn’t add too much. Although it featured a newer version of CLR (the Common Language Runtime), it was mostly to enable side-by-side installation of both versions on the same computer.
The most notable new features were support for IPv6, mobile controls for ASP.NET Web Forms and an out-of-the-box ADO.NET data provider for ODBC. There wasn’t a particularly convincing reason to migrate to v1.1 unless you had a specific need for one of these features.
.NET framework 2.0 was a different story, altogether. It built on the recognized weaknesses of the previous two versions and introduced many new features to boost developer productivity. Let’s name only the ones with the biggest impact:
- The introduction of generics allowed creation of templated data structures that could be used with any data type while still preserving full type safety. Generics was widely used in collection classes, such as lists, stacks, dictionaries etc. Before generics, only objects could be stored in the provided collection classes. When used with other data types, code had to cast the objects back to the correct type after retrieving them from the collection. Alternatively, custom collection classes had to be derived for each data type. Generics weren’t limited to collections, though. Nullable value types were also relying on them, for example.
- Improved debugging allowed edit and continue functionality: when a running application was paused in the debugger, code could be modified on the fly and the execution continued using the modified code. It was a very important feature for existing Visual Basic 6 programmers who were used to equivalent functionality in their development environment.
- 64-bit runtime was added. At the time, only Windows Server had a 64-bit edition and the feature was mostly targeted at server applications with high memory requirements. Today, most of Windows installations are 64-bit and many .NET applications run in 64-bit mode without us even realizing it.
All of these, along with countless other smaller improvements to the runtime and the class library made .NET framework 2.0 much more attractive to Windows developers using other development tools at the time. It was enough to make .NET framework the de-facto standard for Windows development.
.NET framework 3.0 was the first version of .NET framework which couldn’t be installed side-by-side with the previous version because it was still using the same runtime (CLR 2.0). It only added new application frameworks:
- Windows Presentation Foundation (WPF) was the alternative to Windows Forms for desktop application development. Improved binding enabled better architectural patterns, such as MVVC. With extensive theming support, application developers could finally move beyond the classic battleship -gray appearance of their applications.
- Windows Communication Foundation (WCF) was a new flexible programming model for development of SOAP compliant web services. Its configuration options made it possible to fully decouple the web service implementation from the transport used. It still features the most complete implementation of WS-* standards on any platform but is losing its importance with the decline of SOAP web services in favor of REST services.
- Windows Workflow Foundation (WF) was a new programming model for long running business processes. The business logic could be defined using a graphical designer by composing existing blocks of code into a diagram describing their order of execution based on runtime conditions.
While WF wasn’t widely used, we can hardly imagine .NET framework without WCF and WPF and its derivatives.
.NET framework 3.5 continued the trend of keeping the same runtime and expanding the class library.
The most notable addition was Language Integrated Query (LINQ). It was a more declarative (or functional) approach to manipulating collections. It was highly inspired by SQL (Structured Query Language) widely used in relational databases. So much so that it offered a query syntax as an alternative to the more conventional fluent API.
By using expression trees to represent the query structure it opened the doors to many LINQ providers, which could execute these queries differently based on the underlying data structure. Three providers were included out-of-the box:
- LINQ to Objects for performing operations on in-memory collections.
- LINQ to XML for querying XML DOM (document object model).
- LINQ to SQL was a simple object-relational mapping (ORM) framework for querying relational databases.
Web related technologies also started to evolve with this version of .NET framework:
- WF services exposed WF applications as web services by integrating WF with WCF.
- Managed support for cryptography was expanded with additional algorithms: AES, SHA and elliptic curve algorithms.
Looking at the name of .NET framework 3.5 SP1 one would not expect it to include features, but at least two of them are worth mentioning:
- Entity Framework (EF) was a fully featured ORM framework that quickly replaced LINQ to SQL. It also heavily relied on LINQ for querying.
- .NET Client Profile was an alternative redistribution package for .NET framework which only included the parts required for client-side applications. It was introduced as a mean to combat the growing size of the full .NET framework and to speed up the installation process on the computers which did not need to run server-side applications.
.NET framework 4 was the next framework that included a new version of the CLR and could therefore again be installed side-by-side with the previous version.
Note: As mentioned previously, you cannot run versions 2.0, 3.0, and 3.5 side by side on a computer. Apps built for versions 2.0, 3.0, and 3.5 can all run on version 3.5. Starting with the .NET Framework 4, you can use in-process side-by-side hosting to run multiple versions of the CLR, in a single process.
The most notable new additions were:
- Dynamic Language Runtime (DLR) with improved support for late binding. This simplified certain COM interop scenarios and made porting of dynamic languages to CLR easier. IronRuby and IronPython (Ruby and Python ports for .NET framework) took advantage of that.
- Portable Class Libraries (PCL) were introduced for creating binary compatible assemblies that could run on different runtimes (not only .NET framework, but also Windows Phone and Silverlight).
- Task-based Asynchronous Pattern (TAP) was a new abstraction for writing multi-threaded and asynchronous applications which hid many of the error-prone details from the programmers.
- ASP.NET MVC was an alternative programming model to Web Forms for creating web applications. As the name implies, it implemented the model view controller (MVC) pattern which was gaining popularity on other platforms. It was first released out-of-band, but .NET framework 4 was the first version to include it out-of-the-box.
.NET framework 4.5 was the last major new release of .NET framework with several new important features:
- Support for asynchronous I/O operations was added as the new async and await syntax in C# made it much easier to consume them.
- ASP.NET Web API and ASP.NET Web Pages were first included in the framework after they were originally released out-of-band. They allowed development of REST services and provided an alternative Razor syntax for ASP.NET MVC views, respectively.
- PCL was expanded to support Windows Store applications for Windows 8.
Also, .NET Client Profile was discontinued as optimizations to the size and deployment process of .NET framework made it obsolete.
Since .NET framework 4.5, the release cadence increased. The version numbers (4.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.x) reflected that. Each new version brought bug fixes and a few new features. The more important ones since .NET framework 4.5 were:
- Edit and continue support for 64-bit applications.
- Improved debugging for asynchronous code.
- Unification of different ASP.NET APIs (ASP.NET MVC, ASP.NET Web API and ASP.NET Web Pages).
- Support for HTTP/2.
- Improved 64-bit just-in-time (JIT) compiler for faster startup time of smaller applications.
The focus of new development has now shifted to .NET Core, therefore new major improvements to .NET framework are not very likely. We can probably still expect minor releases with bug fixes and smaller improvements, though.
.NET Framework - Backward Compatibility
Backward compatibility has been an important part of .NET framework since its original release. With rare exceptions, any application built for an older version of .NET framework should run in any newer version of .NET framework without any issues.
We can group.NET framework versions into two categories:
- In the first category are .NET framework versions which include a new version of CLR. Some breaking changes were introduced in these versions which could affect existing applications. To keep all applications working in spite of that, these .NET framework versions are installed side-by-side, i.e. they keep the previous version intact and allow applications that were built for it to still use it. There were four versions of CLR released: 1.0, 1.1, 2.0, and 4.
- In the second category are .NET framework versions without a new version of CLR. These only expand the class library, i.e. add new APIs for the applications to use. They install in-place and therefore affect existing applications, forcing them to run on the updated .NET framework version.
Since Windows 8, .NET frameworks 1.0 and 1.1 are not supported any more. The operating system comes preinstalled with the version of .NET framework that is current at the time of release. Newer versions are installed as part of Windows Update. The preinstalled version is based on CLR 4. .NET framework 3.5 (the latest version running on CLR 2.0) can be optionally installed as a Windows feature.
Applications built for .NET framework 4 or newer will run on the preinstalled CLR 4 based .NET framework version. They might still fail to run properly if they use an API from a newer version of .NET framework that is not yet installed on the target computer.
Applications built for older versions of .NET framework will by default try to run on .NET framework 3.5. If it is not installed on the target computer, the application will fail to run and prompt the user to install .NET framework 3.5. This behavior can be changed by adding a supportedRuntime entry to the application configuration file (named MyApplication.exe.config, where MyApplication.exe is the affected executable filename):
<?xml version="1.0" encoding="utf-8" ?>
By specifying multiple runtimes, they will attempt to be used in the order given, i.e. in the above example if .NET framework 3.5 is installed on a machine (v2.0.50727 indicates CLR 2.0), the application will use it, else it will run on the latest .NET framework version installed.
Alternatively, only v4.0 could be listed in the configuration file to force the application to run using the latest version of .NET framework even if .NET framework 3.5 is also installed. If the application is known not to have any issues with the latest version of .NET framework, this will allow it to take advantage of optimizations in CLR 4 without recompiling it.
Multi-Targeting in Visual Studio
To help with development of applications for the .NET framework version of choice, Visual Studio features full multi-targeting support. When creating a new project, there is a dropdown available in the New Project dialog to select the .NET framework version.
This will not only affect how the new project will be configured, but will also hide any project templates that are not supported in the selected .NET framework version (e.g. WPF App cannot be created when .NET framework 2.0 is selected because it was only introduced in .NET framework 3.0).
Figure 1: .NET framework dropdown in New Project dialog
Visual Studio will make sure that only APIs included in the selected.NET framework version will be available to you. If you attempt to use any that are not supported, they will be marked accordingly in the code editor.
Figure 2: Unsupported APIs marked as errors in the code editor
The target framework for an existing project can be changed at a later time on the Application tab of the project properties window. This will have the same effect as if that target framework was already selected when the project was created. If the existing code used APIs, which are not available after the change, the project will fail to build until the code is fixed or the target framework is changed back. The compiler will also warn about any referenced system assemblies that are not available so that they can be removed from the project references.
Figure 3: Changing the target framework for an existing project
Which Version of C# to Use?
No matter which .NET framework version we target in the project, the C# language version in use will not change. That’s fine because the vast majority of language features that were introduced in later versions of the language don’t depend on the CLR or specific APIs. They are only syntactic sugar and the bytecode generated by the compiler will still work in .NET framework 2.0.
There are a couple of exceptions, though.
For example, the async and await keywords are syntactic sugar for Task-based asynchronous pattern, which was only introduced in .NET 4, therefore they cannot be used in earlier versions of .NET framework. They also depend on some additional classes, which are only available in .NET framework 4.5. However, these can be added to the project by installing Microsoft.Bcl.Async NuGet package. This way, you can start using async and await keywords in a project targeting .NET framework 4.
Similarly, some compiler features depend on specific attributes in their implementation:
· To compile code taking advantage of extension methods, System.Runtime.CompilerServices.ExtensionAttribute is required. It was only introduced in .NET framework 3.5.
· Caller information attributes were only added in .NET framework 4.5 and without them we have no way of telling the compiler our intention although the generated code would still work in older versions of .NET framework.
public void LogMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
// logging implementation
Although there is no official NuGet package to make these two features available for use in older versions of .NET framework, the compiler only requires the presence of attributes with the correct fully qualified name to make them work. You can either declare them in your own code or install one of the unofficial NuGet packages.
The above-mentioned limitations don’t prevent you from using the latest version of compiler when targeting an older version of .NET framework, though. The code editor will immediately warn you if you try to use an unsupported feature so that you can avoid it while still taking advantage of all the other supported features.
With that being said, there is still a legitimate use case when you might want to use an older version of the compiler in your project.
Each compiler version is only available from the Visual Studio version which it was originally released with. If some developers on the project are still using an older Visual Studio you will need to avoid using newer language features or they won’t be able to compile the code. To safely achieve that, you can specify the language version in the Advanced Build Settings dialog, accessible via the Advanced… button on the Build tab of the project properties.
Figure 4: Language version selection in Advanced Build Settings dialog
When deploying applications to more strictly managed environments, you might encounter restrictions on what versions of .NET framework can be made available.
Thanks to backward compatibility most of applications developed for older versions of .NET framework will run just fine, if not better on the latest version of .NET framework. They can run even without recompilation as long as you declare that the runtime is supported in the application configuration file.
On the other hand, when targeting an older version of .NET framework, you will need to avoid using APIs which were not yet available. Visual Studio can help you with that if you correctly set the .NET framework version you are targeting in the project properties. You will be able to keep using the latest version of C# though, unless you have team members who are still working on an older version of Visual Studio.