2019 has been a big year for the .NET desktop platforms. First of all, Windows Forms and WPF were open-sourced and included in .NET Core. Originally these two platforms were supported on .NET Framework only and starting from .NET Core 3, desktop developers can also build their applications on top of .NET Core. Besides that, many new features became available for Windows Forms and WPF, such as:

  • Windows Forms and WPF are now able to support a variety of new use cases including APIs for both Windows and devices as well as UWP controls through XAML Islands.
  • MSIX provides a new easy way of packaging, installing, and updating desktop applications.
  • App Center has added support for Windows Forms and WPF, so now developers can benefit from multiple distribution, analytics, and diagnostics services for projects.

In this article, I'll show how you can incorporate all of those new features in your desktop applications and what benefits you will gain from those upgrades.

Why Should Desktop Developers Care About .NET Core?

With the latest version of .NET Core 3.0 released in September 2019, desktop developers have a choice for their .NET runtime. Even though .NET Framework will continue to be fully supported and updated, there are many reasons to consider .NET Core.

.NET Core is the Future for .NET

At Build 2019, Microsoft announced that all new APIs, language features, and runtime improvements will be exclusive to .NET Core in order to protect existing .NET Framework applications from breaking changes. Microsoft also talked about .NET 5 as “the one” .NET platform, coming in 2020. Behind the scenes, .NET 5 is simply the next iteration of .NET Core, so by porting an application to .NET Core, you're preparing for the future of .NET 5.

Innovate at Your Own Pace

.NET Framework only allows a single version of the framework to be installed on each computer at any given time, and this runtime is shared by all applications that depend on .NET. If a user updates to the latest version of .NET Framework, all .NET applications on that computer will be similarly updated. Within the enterprise, this often means that the framework updates are a company-wide rollout initiated by their IT organization which usually leads to lengthy delays in updating .NET Framework.

Using .NET Core, you can control the version of .NET Core for an application. This allows you to ensure a stable runtime environment for the users.

With the ability to package a version of .NET Core along with an application, the execution environment can be custom tailored. This gives you freedom to upgrade to the latest versions of the .NET platform at your own pace without delays created by customers' update cycles.

Smaller App Sizes with Assembly Trimming

Including the entire runtime within the installation package seems like a daunting dependency to push onto users. Fortunately, an application will likely require only a small subset of the .NET Core libraries to function. The .NET Core 3.0 SDK comes with a tool that can analyze intermediate language (IL) and trim unused assemblies, which may reduce the size of an application.

To enable this feature, add this setting in the project and publish the application as Self-Contained:

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>  

Single-File Executables

A new .NET Core feature allows you to package your application into a platform-specific, single file executable. This self-extracting archive provides a lightweight deployment package for the users.

To publish a single-file executable, set the PublishSingleFile in the project or on the command line with the dotnet publish command (please note that the line is broken only to accommodate the narrow columns in the printed magazine):

dotnet publish -r win10-x64 /p:PublishSingleFile = true

Or from a console:

<PropertyGroup>
  <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
  <PublishSingleFile>True</PublishSingleFile>
</PropertyGroup>

Step-by-Step Migration from .NET Framework to .NET Core

You can follow along in the demo with code at this repository: https://github.com/microsoft/WPF-Samples/tree/master/Sample%20Applications/PhotoStoreDemo. I'll migrate a simple WPF application called Photo Store that offers photo sales. The main form of this application is shown in Figure 1.

Figure 1: Photo Store application that you'll be porting to .NET Core
Figure 1: Photo Store application that you'll be porting to .NET Core

The Photo Store application allows users to select products, edit their images, choose from a variety of printing options, and place their order. The application targets .NET Framework 4.7.2 and has a single form with embedded JPEG resource files.

Because the application is using the Path API for getting access to the pictures and the path to the executable is slightly different for .NET Core and .NET Framework, let's make these three simple changes that will help facilitate the change between .NET Framework and .NET Core:

Before starting the migration, it's important to evaluate how compatible the application will be with .NET Core 3.

Estimate the Porting Cost

Microsoft's Portability Analyzer (https://aka.ms/portabilityAnalyzer) determines whether an application depends on APIs that aren't supported in .NET Core. This tool provides a Portability Report, which can be seen in Figure 2.

Figure 2: Portability Report for the sample application
Figure 2: Portability Report for the sample application

The first tab, Portability Summary, shows the calculated compatibility score. A score of 100% in each row indicates that an application is fully compatible with .NET Core. When deficiencies belong to NuGet packages, check to see if that package supports .NET Core. Once the application has been retargeted, NuGet restore automatically pulls the correct .NET Core-compatible version of the package. When deficiencies belong to assemblies, you'll need to refactor the code to avoid using unsupported APIs. Go to the second tab, Details, filter by assembly name to hide all NuGet packages, and walk through the list.

In this case, Photo Store has 100% compatibility so you can port it to .NET Core. Below, I provide instructions on how to do it by hand, but first I recommend trying a Try Convert, which might be able to do all of the work for you. Because project files for various applications may be very different, it's impossible to create a silver bullet solution that covers all cases. This tool will cover the most popular cases and work just fine for the majority of the projects. If it didn't work for you, go ahead and try to do it manually.

Porting with Try Convert

Try Convert is a global tool that tries to convert your project file from the old style to the new SDK style and updates the target framework version for your application from .NET Framework to .NET Core. You can install it from here: https://github.com/dotnet/try-convert/releases. Once installed, in CLI, run the command:

dotnet try-convert -p "<path to your .csproj file>"

After the tool completes the conversion, reload your files in Visual Studio. Check in the properties of your project to see if it's now targeting .NET Core 3.0.

Porting by Hand

.NET Core requires project files to have the new SDK style. If the application was created on the .NET Framework, the project file has the old style, like the Photo Store sample. First, you need to check in the Solution Explorer to see if the project contains a packages.config file. If so, right-click on it and select Migrate packages.config to PackageReference from the context menu. This moves the dependencies from the packages.config to the project and ensures that you won't lose them when updating to the new-style project file

Create a copy of your current .csproj file because you'll need it in the future. After the copy is made, open the current .csproj file and replace all the content with the following code.

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net472</TargetFramework>
    <UseWPF>true</UseWPF>
    <GenerateAssemplyInfo>false</GenerateAssemplyInfo>
  </PropertyGroup>
</Project>

For Windows Forms applications, specify <UseWinForms> rather than ``` like this:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net472</TargetFramework>
    <UseWinForms>true</UseWinForms>
    <GenerateAssemplyInfo>false</GenerateAssemplyInfo>
  </PropertyGroup>
</Project>

Note that <GenerateAssemblyInfo> should be set to false. For new projects, AssemblyInfo.cs is generated automatically by default. So, if you already have an AssemblyInfo.cs file in the project, you need to either delete the file or disable auto-generation.

To not break the project, you need to add the references and resources from the old version of the project file (that's why you saved it) to the new project file. Copy and paste all lines related to <PackageReference>, <ProjectReference> and <Content> from the saved copy.

Save everything, build, and run. You're still targeting .NET Framework, but your project file has the new SDK-style format. Now you're ready for porting. Open the project file, find the property <TargetFramework>, and change the value from net472 to netcoreapp3.0.

Build and run your project. Congratulations, you ported to .NET Core 3!

Fixing Migration Errors

Errors such as “The type or namespace (...) could not be found” or “The name (...) does not exist in the current context” can often be fixed by adding a NuGet package with the corresponding library. If you can't find the NuGet package with the library that's missing, you might find the missing APIs in the Microsoft.Windows.Compatibility NuGet package that has ~21,000 .NET APIs from .NET Framework.

Building for Both .NET Core and .NET Framework

In cases where an application needs to be built for both .NET Framework and .NET Core, there are two common patterns that can be employed. First, for small blocks of code that need to differ between .NET Core and .NET Framework, the #if directives work with automatically defined preprocessor symbols. NETFRAMEWORK, NETCOREAPP, NETSTANDARD, and more specific symbols like NET472 can be used. are defined when targeting .NET Core and .NET Standard, respectively. (See more at https://docs.microsoft.com/dotnet/core/tutorials/libraries#how-to-multitarget).

What Doesn't Work in .NET Core Out of the Box?

Many projects can be ported just like the example I just discussed, but there will be some applications that require more refactoring. Let's talk about what's not available in .NET Core and how to address those dependencies.

Configuration Changes

New .NET Core apps typically use the Microsoft.Extensions.Configuration package to load configuration settings from JSON, environment variables, or other sources. For migration purposes, the ConfigurationManager APIs are available in the System.Configuration.ConfigurationManager package (or in the Microsoft.Windows.Compatibility compatibility pack). Loading app settings, connections strings, or custom configuration sections from ConfigurationManager will work the same as before, but app config files are no longer used to configure .NET features.

.NET Core doesn't have a machine.config file to define common configuration sections like system.diagnostics, system.net, or system.servicemodel, so an app's config file will fail to load if it contains any of these sections.

Common features areas affected by this change are System.Diagnostics tracing and WCF client scenarios. Both of these feature areas were commonly configured using XML configuration previously and now need to be configured in code instead. To change behaviors without recompiling, consider setting up tracing and WCF types using values loaded from a Microsoft.Extensions.Configuration source or from appSettings.

The app's config file will now be named AppName.dll.config rather than AppName.exe.config because .NET Core applications have their app code in DLLs (the .exe that is created at build-time is actually the host that starts the .NET Core runtime and loads the application from a neighboring .dll file). In most cases, this file name is unimportant because ConfigurationManager loads it automatically.

WCF Client

WCF client code that was auto generated by SvcUtil will need to be regenerated for use with .NET Core. Although many WCF client APIs are available for .NET Core, the clients generated by SvcUtil depend on XML configurations that don't work.

There are two ways to generate a new .NET Standard-compliant WCF client. Dotnet- SvcUtil is a .NET CLI tool that can create WCF clients from the command line (https://docs.microsoft.com/dotnet/core/additional-tools/dotnet-svcutil-guide).

WCF clients can also be generated with Visual Studio's Connected Services (https://docs.microsoft.com/en-us/dotnet/core/additional-tools/wcf-web-service-reference-guide). Like dotnet-svcutil, this auto-generates the necessary client code in a file called Reference.cs.

Projects that generate WCF clients manually using ClientBase<T>, ChannelFactory<T>, or custom Channel/ChannelFactory implementations should continue to work. Some APIs that aren't supported on .NET Core will need to be replaced with alternate APIs (e.g., NetNamedPipeBinding, WS-* bindings, and message-level security), but most of the WCF client surface area is available on .NET Core.

Note that WCF client APIs are supported on .NET Core, but WCF server APIs aren't. If an app uses ServiceHost or other server-side WCF APIs to host services, that code doesn't run on .NET Core. The community-owned Core WCF project (https://github.com/CoreWCF/CoreWCF) and alternative technologies like gRPC or ASP.NET Core can be considered.

Code Access Security

.NET Core differs from .NET Framework in that all code is loaded as fully trusted and security critical. In recent versions of the .NET Framework, Code Access Security (CAS) and Transparency attributes are no longer considered security boundaries. .NET Core further deprecates these systems.

Security-related APIs are still present in .NET Core (CAS types are in the System.Security.Permissions package), but they're no longer necessary. Transparency attributes (SecurityCritical, SecuritySafeCritical, and SecurityTransparent) are present but have no effect. Similarly, asserting or demanding for permissions always succeeds.

Some other CAS APIs will need to be removed. Any call that previously would have restricted permissions (PermissionSet.Deny or PermissionSet.PermitOnly, for example) now throws a PlatformNotSupportedExcption to alert the user that permissions aren't being restricted as they would have been in .NET Framework. In these cases, the CAS-related calls need to be removed and the code should be reviewed to make sure it's ok for the scenario to run in full trust. In some cases, API overloads that took CAS-related arguments are missing in .NET Core, but they can be safely replaced with other overloads that don't take these arguments.

If applications need to run with restricted access, security boundaries in the operating system (virtualization, containers, user accounts, or app capabilities) can be employed. APIs for interacting with OS-level security concepts (e.g., ACLs APIs) still work as before, though some methods may come from different (Windows-only) NuGet packages. https://apisof.net/ is a useful tool for checking APIs (which usually maps to a NuGet package of the same name).

App Domains

An important architectural difference between .NET Framework and .NET Core is that .NET Core only has a single app domain. Applications that create app domains will need to be modified to be compatible. Most AppDomain APIs are still available in .NET Core, so querying the current domain settings or adding an unhandled exception handler should work in many cases. APIs related to creating new app domains will throw an exception.

There are a few reasons that .NET Framework apps typically create app domains:

  • Running code with reduced permissions
  • Enabling the loading of multiple copies of an assembly in isolation
  • Making it possible to unload assemblies

Restricting permissions is no longer necessary in .NET Core as all code is run as Fully Trusted. The other scenarios can be accomplished with Assembly Load Contexts in .NET Core, logical containers that assemblies are loaded into. These allow one version of an assembly to be loaded in one context while a different version can be loaded in another (which is valuable for isolating different components in a plugin architecture). Beginning in .NET Core 3, Assembly Load Contexts (and the assemblies in them) can be unloaded. If an app is using app domains to for code isolation or to unload assemblies, consider re-working the application to use the AssemblyLoadContext type instead (https://docs.microsoft.com/dotnet/standard/assembly/unloadability-howto).

One important difference between app domains and assembly load contexts is that unloading an app domain forced all code in the domain to stop, whereas unloading an Assembly Load Context is cooperative. The unload doesn't happen until no threads have assemblies from the load context on their call stack and there are no live references to types from those assemblies. It's important to make sure that the assemblies from the load context are no longer in use when attempting to unload.

Interop

If an app needs to interoperate with native components, most of the same technologies that worked on .NET Framework will work on .NET Core with minor tweaks. Platform invokes (p/invokes) are one of the easiest ways to call native functions from managed code and are the only interop technology supported cross-platform. Although Windows Forms and WPF apps will only run on Windows, libraries that may be used by other (potentially cross-platform) .NET Core apps should prefer p/invokes to communicate with native dependencies.

.NET Core apps that only target Windows can also make use of COM or C++/CLI for interop. The cl.exe compiler that ships with Visual Studio 2019 16.3 includes a new command-line option for compiling C++/CLI code for .NET Core: /clr:netcore. When linking C++/CLI binaries targeting .NET Core, it's also necessary to include the .NET Core's ijwhost.dll library in the linker's libpath. Beginning with Visual Studio 2019 16.4, the ability to target .NET Core from C++/CLI projects will be supported in MSBuild and the Visual Studio IDE.

New Windows 10 APIs can be used with WinRT interop, as explained later in this article. The System.EnterpriseServices namespace is not supported on .NET Core.

Remoting

.NET Core doesn't support remoting APIs, and usage of those APIs will need to be replaced with alternatives. Remoting has frequently been used for cross-app domain communication in the past, but because .NET Core apps only have a single, default app domain, these scenarios are no longer applicable. In other cases, remoting was used for inter-process communication (IPC). Simple IPC scenarios can be re-written using System.IO.Pipes or System.IO.MemoryMappedFiles APIs. More complex IPC interfaces or scenarios involving network communication can be handled by ASP.NET Core, socket-based communication (System.Net.Sockets), or gRPC (https://docs.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start).

Although most remoting APIs are unavailable on .NET Core, there are a couple of specific APIs that are worth highlighting. One is System.Runtime.Remoting.Proxies.RealProxy. RealProxy is often used to create wrappers that handle cross-cutting concerns (logging, caching, etc.) for other types using aspect-oriented programming patterns. In these cases, the remoting capabilities of RealProxy aren't needed, but the type is unavailable in .NET Core because of its remoting underpinnings. System.Reflection.DispatchProxy was added to .NET Core to fill this gap. If an app uses RealProxy to wrap objects and intercept calls to them, DispatchProxy can be used as a .NET Core-compatible replacement.

Finally, it's worth mentioning the asynchronous programming model methods Delegate.BeginInvoke and Delegate.EndInvoke(IAsyncResult). The asynchronous programming model uses remoting in its implementation and, consequently isn't supported on .NET Core. Calling BeginInvoke on a delegate in a .NET Core app results in a PlatformNotSupportedException. The more modern task-based asynchronous pattern (TAP) is recommended instead. If an app is using BeginInvoke, it can be updated to use tasks instead (https://devblogs.microsoft.com/dotnet/migrating-delegate-begininvoke-calls-for-net-core).

Adding New Capabilities to Desktop Applications with Windows 10

In addition to all the value.NET Core brings to Windows desktop developers, it's now easier than ever before to bring new features to applications with capabilities supported in Windows 10. Some of these new improvements include making it easier to deploy and update applications, add new UI, or access Windows 10 device and platform APIs.

MSIX Packaging

At Build 2018, Microsoft announced an update to its packaging and deployment technology for applications on Windows, MSIX. This new packaging and distribution system, based on a combination of .msi, .appx, App-V, and ClickOnce, brings a host of helpful features to build a more modern software distribution system for developers building Windows applications. Some of the features that make MSIX stand out include:

  • Background application updates
  • Delta package updates
  • Declarative installation
  • Forced updates
  • Clean uninstalls
  • Disk space optimization
  • Streaming installs
  • Device targeting
  • Tamper protection
  • Open sourced on GitHub (https://github.com/microsoft/msix-packaging)

The most recent releases of Visual Studio make it simple to generate MSIX packages for .NET Core desktop applications. With a new project template called the Windows Application Packaging Project, you can easily configure package metadata using the package manifest designer and generate packages through the Publish > Create App Packages context menu. Where .NET Core and MSIX shine is with the deployment flexibility of .NET Core. With the Windows Application Packaging Project, MSIX, and .NET Core desktop applications, developers can create a self-contained application with no external dependencies. In other words, the .NET Core runtime is contained within the application package.

XAML Islands

In addition to streamlined deployment, desktop developers now have a way to leverage modern XAML capabilities in their existing Windows Forms and WPF applications without needing to rewrite their applications from scratch. Microsoft is bringing over a decade of UI XAML innovation to all developers on Windows via XAML Islands (https://docs.microsoft.com/windows/apps/desktop/modernize/xaml-islands).

XAML Islands instantiate an object at runtime derived from HwndHost to contain arbitrary WinUI XAML (previously known as UWP XAML). This technology can be used to host a specific, advanced control (such as the Windows 10 Maps control), or developers can replace entire forms or pages of UI with more complex compound controls. Although this technology is a great solution for .NET Core desktop developers, it's really a tool to enable any developer on Windows to incrementally bring a modern UI to their Win32 applications.

A few examples of applications built from the ground up with XAML Islands are the new Windows Terminal (https://github.com/microsoft/terminal) as well as PowerToys (https://github.com/microsoft/PowerToys).

The future of UI on Windows is the Windows UI Library, also known as WinUI (https://aka.ms/WinUI). Although XAML Islands is a great option for new applications today, as of this writing, it's only supported on Windows 10 version 1903 and higher. WinUI aims to make this available to a larger audience of Windows developers by decoupling much of the UI platform from the Windows runtime and bringing support to earlier versions of Windows 10. In their public roadmap, WinUI 3.0 aims to bring support to the Creators Update of Windows 10, as well as limited support back to Windows 8.1. WinUI is also being developed on GitHub and priorities and features are directly impacted by community feedback (aka.ms/winui). You can see it in action in Figure 3.

Figure 3: XAML Islands in Action with the new Windows Terminal
Figure 3: XAML Islands in Action with the new Windows Terminal

WinRT API Access

In addition to modern distribution and UI, all .NET Windows developers can easily reference Windows 10 platform and device APIs through a NuGet package (https://www.nuget.org/packages/microsoft.windows.sdk.contracts). Adding features that take advantage of Bluetooth, GeoLocation, Cameras, and more are now just a NuGet package away. For a complete reference on the UWP namespaces available to developers, view the API browser at https://docs.microsoft.com/uwp/api/.

SetPoint Medical is an example of a company who leverages the best of .NET and Windows (https://customers.microsoft.com/en-us/story/744483-setpoint-medical-discrete-manufacturing-net). They wanted to bring an existing WPF application used in device manufacturing and testing forward without rewriting major portions of their application. Newer iterations of their hardware only supported Bluetooth to communicate with a PC or external device, and SetPoint was able to easily add built-in Windows Bluetooth capabilities to their existing WPF application to satisfy their requirements.

Continuously Release and Monitor Applications with the App Center

Earlier this year, App Center introduced support for WPF and Windows Forms applications, targeting both .NET Framework and .NET Core. Their aim is to help teams build better apps by bringing together services like distribution, analytics, and diagnostics all under one easy solution.

Manage Releases

One of the best ways to continuously improve an application is by getting an application into the hands of users as quickly as possible. Unfortunately, fragmented processes and tools can make managing releases timely and complicated.

App Center Distribute is a simple and easy to use a solution that allows developers to quickly release an application and manage which version your testers and end users receive. Developers can create different distribution groups and invite their users via email to easily manage your releases.

A developer can upload an application package (.msix, .msi, .msixupload, .msixbundle, .appx, .appxupload, .appxbundle, .zip) to App Center, select a distribution group, specify any release notes and users will receive an email with a link to download the app.

Monitor Application Analytics

With the App Center Analytics, developers can gather insights to better understand an application usage, growth, and trends. By simply integrating the App Center SDK, data will start flowing into the portal. Developers can also track custom events and attach properties to get a deeper understanding about the actions that users take in an application, as shown in Figure 4.

Figure 4: Overview of the application activities in App Center.
Figure 4: Overview of the application activities in App Center.

Diagnose Application Health

App Center's Diagnostics SDK collects crash and error logs and displays them with analysis in the App Center portal. The issues are grouped and provide insights such as the number of occurrences and users, types of device affected, and events that occurred before the crash.

Figure 5: Diagnostics tab in the App Center
Figure 5: Diagnostics tab in the App Center

Getting Started with App Center

The App Center aims to empower you to do you best possible work by providing the tools, data, and insights that you need to focus on coding rather than managing processes or digging through different tools for insights.

You can get started with App Center by creating an account at https://appcenter.ms.

Try .NET Core 3 and the New Desktop Features and Give Microsoft Your Feedback

Microsoft is encouraging developers to give their feedback and participate in the product development. You can send your questions to netcore3modernize@microsoft.com and submit bugs and feature requests on WinForms and WPF repositories.

Here are a few useful links: