In this long read we explore the prospect of upgrading your existing .NET apps to the latest version. We outline the different migration approaches to consider, then discuss some of the key factors relevant to ‘lift and shift’ (architecture-preserving) migrations. This long read is sampled from Hammad Arif’s and Habib Qureshis’s book Adopting .NET 5
Upgrading Existing .NET Apps to .NET 5
Upgrading an app to its latest platform version, just for the sake of upgrading, is usually not a fun exercise, especially if there is no new and exciting feature to be added. Fortunately, Microsoft has provided solid inter-compatibility between libraries developed for earlier versions of .NET and .NET 5 to make the migration process much smoother. The cherry on top is the underlying performance enhancement that will make the existing code run faster without any significant transformation by the app developer.
There are some technologies that Microsoft has decided not to port to the .NET 5 platform, such as Windows Communication Foundation (WCF) Server, Windows Workflow, and ASP.NET web forms. We will discuss the options to replace these technologies where it is feasible to do so.
In this article, you will learn about the following topics:
- Choosing the migration approach
- Points to ponder regarding an as-is upgrade
We’ll assume that you are already familiar with .NET Framework and Entity Framework as we are upgrading these to the latest platform. We will focus on the core aspects of these technologies that require transformation during migration to .NET 5.
Choosing the migration approach
Before starting the migration process there are some big decisions that need to be made upfront.
If you are upgrading from any previous version of .NET Core, the implications are less severe simply because .NET 5 is a continuation of the same platform and the upgrade process is much simpler.
Migrating from .NET Framework, however, requires a bit more thought. There are a number of factors that will impact the approach, including the following:
- The size and footprint of the solution
- The number of different technologies that comprise the solution (for example, desktop apps, web apps, and REST APIs)
- Whether an unsupported technology (such as WCF Server) or .NET runtime feature (such as .NET Remoting) is used
These factors can influence the migration approach, and in particular the decision about whether to migrate the whole solution at once or try to do it in different stages. Let’s review the pros and cons of both approaches.
Big bang approach
For small- and medium-sized solutions, it makes sense to upgrade the entire solution stack at once from .NET Framework to .NET 5. For large solutions, it might not be too practical due to time constraints. You would need to maintain two separate code bases, one each for .NET Framework and .NET 5, during the migration process, and if any new feature needs to be implemented, it could become hard keeping the two code bases in sync.
The benefit of this approach is its simplicity. Instead of worrying about compatibility issues between intermediate phases of the upgrade, we just have to ensure that the final version of all the solution pieces is compatible with .NET 5.
An alternative approach is to identify the least-dependent libraries first. These are the projects that do not depend on other projects in the code base. A common example of such projects is a library containing model definitions or shared helper functions. Once these libraries have been upgraded to .NET 5, we look for the next set of libraries that are only dependent on previously upgraded components. We then gradually move up the dependency ladder to migrate the components with all their dependencies previously migrated.
The .NET team has made it easier by making it possible to multi-target an assembly for multiple platforms. So, you can write code once and target it for both .NET Framework and .NET 5 platforms. This will obviously work for scenarios where feature sets across both platforms are the same, or similar. Once the shared libraries have been migrated, the executing app (desktop or web app) can be migrated to the latest platform to take advantage of .NET 5’s specific features. The complexity arises with this approach when a particular API is not supported on .NET 5. We would have to write a conditional compilation code so that unsupported API is only used when code is compiled for the .NET Framework platform.
Once the choice between gradual versus big bang approach has been made, the next decision is whether to introduce any changes in the solution architecture.
Lift and shift versus architectural transformation
In the lift and shift approach, we try to minimize any fundamental changes to code and its constituent components. Once the application is up and running on the latest platform, architectural changes are taken aboard as a separate project. This is a simpler approach and there is less risk of upgrade failure, but the new platform must support the components from the previous platform.
For example, if you have built a WCF Server instance or have implemented inter-process communication using .NET Remoting, it is simply not possible to upgrade these technologies on an as-is basis on .NET 5 as .NET 5 does not support them.
Architectural transformation makes sense when new features are expected to be implemented as part of an upgrade exercise or in the near future. It is best to leverage the latest platform features and technologies to reduce the technical debt on an older platform.
The final decision that we need to make is about whether and how we upgrade third-party libraries.
Replacing third-party libraries with platform-provided features
A similar concern to the architectural changes that are discussed in the previous section is about making code improvements. This is especially tempting for third-party libraries when a newer alternative is provided by the framework itself.
In .NET Core and .NET 5, Microsoft provides a system API for JSON parsing and serialization named System.Text.Json. Similarly, libraries for dependency injection (DI), Microsoft.Extensions.DependencyInjection, and logging, Microsoft.Extensions.Logging, eliminate the need to use corresponding third-party libraries, especially for basic use cases. If you are using advanced features of existing libraries, then it is best to consult the documentation of the newer alternative to understand whether every feature is available as a drop-in replacement or whether significant development is required.
Also, sometimes it takes less effort to transform the feature than to keep trying to use the existing code or library in a backward-compatible fashion. For example, in .NET 5, it is easier to read application configuration from JSON files instead of *.config XML files, so it may be worthwhile upgrading the configuration reading code unless it is significantly customized in the existing code base.
We will now review the points that would generally be applicable to most .NET like-for-like migration projects.
Points to ponder regarding an as-is upgrade
A transformation project can easily get quite large and complex, but sometimes it is possible to score some worthwhile technical improvements in the solution, where comparatively little effort brings good rewards in terms of code maintenance and performance.
With that in mind, the following subsections make some general points that will be applicable to most upgrade projects.
While most of the .NET Framework technologies can be migrated relatively easily to the .NET 5 platform, some features and technologies do not have a direct replacement and will require redesign or, at a minimum, replacing code from a different library. In addition, due to the fast-paced nature of .NET Core/.NET 5 development, many features in libraries such as Entity Framework Core or ASP.NET Core ASP.NET Core get marked as obsolete or deprecated in newer versions. So, it is best to review release notes when migrating to newer versions of these libraries.
In this section, we will review what major .NET Framework features are not available and the possible approaches to replace these features.
WCF was originally released in November 2006. Since then, it has been a very popular inter-service communication mechanism for .NET developers. It has vastly improved technology compared to ASMX web services. Its major strength is the support for various communication protocols, inter-server communication, and intra-server communication with minimal coding changes.
While still used in many legacy applications, Microsoft has decided to not support WCF Server on the .NET Core/.NET 5 platform in favor of modern technologies such as REST APIs over HTTP or Google RPC (commonly known as gRPC) for contract-based RPCs. Both alternatives are cross-platform and support the most popular programming languages and cloud platforms.
Tip: If you still want to continue development with WCF Server, one option is to consider the CoreWCF project CoreWCF project, which is an open source port of WCF to the .NET Core platform. At the time of writing, this project is still under development and not production-ready.
The recommended approach is to use the REST APIs or gRPC for new features. Alternatively, you can upgrade the WCF client to .NET 5 and continue to use the WCF Server on .NET Framework until you have the time resources available to do the migration.
ASP.NET Web Forms
ASP.NET Web Forms is another technology that is being axed in .NET 5. Microsoft recommends migrating to Blazor WebAssembly for modern web UX development. Blazor supports C# and is quite scalable. The transformation from Web Forms to Blazor is not a simple task and requires considerable planning.
The following Microsoft article has guidance on transforming ASP.NET Web Forms apps to Blazor WebAssembly: https://docs.microsoft.com/en-us/dotnet/architecture/blazor-for-web-forms-developers/migration.
Windows Workflow Foundation (WWF or WF)
WWF’s primary feature set is to reduce coding for developing and automating repeatable business processes. It does so by providing an API, an in-process workflow engine that supports long-running processes, and a visual designer.
Compared to WCF Server and Web Forms, WWF’s market footprint is somewhat reduced. There is no direct alternative to this technology on .NET 5, but most of its functionality can be redesigned using a combination of gRPC, REST APIs, Azure Logic Apps, and Power Automate.
Like CoreWCF, there is an open-source fork of WWF available that is still in the experimental phase: https://github.com/UiPath-Open/corewf.
We will now review some Windows platform-specific features that cannot be used from code when targeting .NET 5.
Unsupported Windows OS-specific features
Aside from the technologies mentioned in the preceding section, some platform-specific features for Windows OS are not available in .NET 5 platform libraries. These features should generally be of concern to Windows desktop developers when migrating to .NET 5.
AppDomains were primarily used to isolate application processes from each other. For modern applications, a better alternative for process isolation is to use containers, which are more versatile and flexible. There is also the AssemblyLoadContext class, which supports dynamically loading assemblies at runtime.
For ease of migration, .NET 5 provides some of AppDomains’ APIs; however, not all methods are guaranteed to behave in the same way. Some will throw PlatformNotSupportedException, some will do nothing, and others will continue to function as per previous behavior.
A related feature to AppDomains is remoting, where processes living in different app domains can communicate with each other. This is no longer supported.
For communications across the network, REST and gRPC are recommended. For interprocess communication on the same machine, MemoryMappedFile or System.IO.Pipes can be used.
Code Access Security (CAS)
Sandboxing applications on the Windows platform allowed applications to execute in a constrained environment with restricted access to resources. This feature has been retired from .NET Framework itself, and so has not been ported to .NET Core or .NET 5.
The cross-platform alternative is to restrict process privileges by using containers and/or constrained user accounts.
Security transparency was used in sandboxed applications to declaratively specify access privileges required by code. This is generally used in Silverlight applications. As CAS is not supported, the related feature of security transparency is also not supported. The alternative is also the same as CAS, which is to use virtualization, containers, and user accounts to control the process privileges.
Transforming application configuration
In .NET Framework, application configuration is typically specified in app.config or web.config files in XML format.
An example appSettings section appears as follows:
.NET Framework implemented a hierarchy of configuration files, where a parent file’s settings can be inherited or overridden by a more specific file. For example, machine-specific settings can be specified in machine.config and then a web application can inherit or override these settings for its process in a web.config file.
These settings are accessed by developers using the System.Configuration namespace. This namespace is still available in the .NET Core world, but a more flexible approach with usually minor code changes is to use the Microsoft.Extensions.Configuration namespace, which offers considerable improvements.
The benefits of using the Microsoft.Extensions.Configuration namespace include:
- Support for hierarchical plain old CLR object (POCO)-based settings
- Built-in binding and validation for non-string values, such as int, bool, and decimal
- Uniform handling and choice of configuration store, such as JSON files, XML files, in-memory objects, command-line arguments, and environment variables
- Support for encrypted and cloud-specific stores, such as Azure Key Vault or Azure App Configuration
Choosing the Entity Framework version
The majority of applications need to access databases for one use case or another. Entity Framework is a very popular choice for .NET developers as an object-relation mapping (ORM) tool to access the data layer in .NET Code.
The most recent major version of Entity Framework is Entity Framework 6. It is fully supported by Microsoft on the .NET Core platform, but all new development should be done on Entity Framework Core. EF Core supports .NET Framework (up to EF Core v3.1) and .NET 5 (all EF Core versions).
EF Core and Entity Framework do not have 100% feature parity; there are many new features in EF Core and some that will never be ported from Entity Framework to EF Core (due to low usage or radical design change), while some features are slated to be ported in future EF Core versions.
Refer to the official documentation for a detailed overview of the features that are not available on either platform: https://docs.microsoft.com/en-us/ef/efcore-and-ef6/.
Using .NET Framework compatibility
A common migration approach is to port your libraries to .NET Standard first. .NET Standard libraries can be referenced from both .NET Framework and .NET 5 assemblies, which works great as an interim solution until both frameworks are in use in the organization.
With .NET Framework compatibility mode, .NET Standard projects can make references to .NET Framework libraries as if they were compiled for the .NET Standard platform. This helps remove a major migration blocker where migrating dependent libraries to newer platforms is not feasible in a short time frame.
Of course, having a compatibility layer does not automatically guarantee that all .NET Framework APIs will work on the .NET 5 platform. The APIs that are not supported will either throw PlatformNotSupportedException or might do nothing in some cases.
The vast majority of APIs are now ported on the .NET 5 platform, so using .NET Standard is a useful way to reference third-party NuGet packages that have not been updated by their developers despite having full API availability on the new platform.
During our example migration exercise, we will learn about using the .NET Portability Analyzer tool, which can detect and inform about potential API incompatibility on the new platform.
Upgrading third-party NuGet packages
When using third-party NuGet packages, you can use one of the following approaches:
- If the current NuGet package version is .NET 5- or .NET Standard 2.1-compatible, then use that version.
- If the current version does not support .NET 5/.NET Standard 2.1, but the later NuGet package version does, it’s likely a good candidate to be used. We need to review any breaking changes in the new version to assess the code refactoring effort required to use the new version.
- The vast majority of active and popular NuGet packages fall under the first two cases. However, if there is no compatible version, then it is possible to use the .NET Framework library from .NET Standard code using the compatibility mode as described in the preceding section.
- Another option is to use an alternative library, but this could potentially take significant time and cost to implement.
We have now covered enough theoretical ground to understand the considerations and pitfalls of migration. Let’s now dive into a practical example of upgrading a fictional real-world app.
In this long read, we’ve learned about different migration approaches and the pros and cons of each. There is no one-size-fits-all solution. The preferred approach depends on the compatibility of the technologies involved, the size of the existing code base, and the extent of dependence on the third-party NuGet libraries.
Sometimes it’s not possible to use the legacy technology, as we saw in the case of WCF Server, WWF, and Web Forms. Other technologies, such as gRPC, Blazor, and REST, can usually be used to replace the existing solutions in such cases.
A practical guide to building and upgrading new and legacy applications on cloud-native platforms using architectural best practices with .NET 5, C# 9, microservices, and ML.NET
○ Get up to speed with .NET 5’s new improvements and features
○ Discover how to improve existing code design and enhance software maintainability
○ Explore explanations and techniques for making programs easier to understand and change
Learn how to build web applications efficiently using ASP.NET Core 5 with the C# programming language and related frameworks
○ Build web apps and services and cross-platform applications using .NET and C#
○ Understand different web programming concepts with the help of real-world examples
○ Explore the new features and APIs in ASP.NET Core 5, EF Core, Visual Studio, and Blazor
AA guide to discovering the hidden behaviors of ASP.NET Core that can be customized to optimize your .NET 5 applications
○ Customize the default behavior of ASP.NET Core to get the most out of the framework
○ Enhance the app configuration, change the default dependency injection, and build your own tag helpers
○ Discover best practices for configuring ASP.NET Core, from user interface design to hosting it on platforms