Connect(); 2017

Volume 32 Number 13

.NET - Introducing the Windows Compatibility Pack for .NET Core

By Immo Landwerth; 2017

The Microsoft .NET Framework is still the best choice for certain styles of apps, especially for desktop apps and Web apps that use ASP.NET Web Forms. But if you need highly scalable Web apps, create self-contained deployments using Docker, or if you need to run on Linux, then you want to consider porting to .NET Core. But bringing existing code to .NET Core can be a challenge. In this article, I’ll explain how you can use the new Windows Compatibility Pack for .NET Core. It provides access to APIs that were previously available only for .NET Framework (for example, System.Drawing, System.DirectoryServices, ODBC, WMI and many more). Because this includes both cross-platform and Windows-only technologies, it’s critical to understand early if you’re using APIs that might interfere with your cross-platform goals. I’ll address this by showcasing the new API analyzer, which gives you live feedback as you’re editing code.

Overview

When we shipped .NET Core 1.x, as well as .NET Standard 1.x, we were hoping to be able to use this as an opportunity to remove legacy technologies and deprecated APIs. Since then, we’ve learned that no matter how attractive the new APIs and capabilities of .NET Core are, if the existing code base is large enough, the benefits of the new APIs are often dwarfed by the sheer cost of reimplementing or adapting that code. Luckily, the Windows Compatibility Pack provides a good chunk of these technologies so that building .NET Core applications and .NET Standard libraries becomes much more viable for existing code.

This capability is a continuation of .NET Standard 2.0 (bit.ly/2iuekmN), in which we significantly increased the number of APIs that can be shared across all .NET implementations, especially .NET Core. The goal was to make porting existing code much easier and ensure it largely compiles just as is. However, we also didn’t want to complicate .NET Standard by adding large APIs that can’t work across all platforms, which is why we haven’t added the Windows registry or reflection-emit APIs. Because the Windows Compatibility Pack is a separate NuGet package and sits above .NET Standard, it’s free to provide access to technologies that are Windows-only.

Providing more APIs for class libraries that target .NET Standard also helps with the compatibility mode we’ve added in .NET Standard 2.0. This compatibility mode allows the referencing of existing .NET Framework binaries, which helps with the transition period where many packages aren’t yet available for .NET Standard or .NET Core. But this compatibility mode doesn’t change physics: It can only bridge differences in assembly names between .NET Framework and .NET Standard. It can’t give you access to APIs that don’t exist for the .NET implementation on which you’re running. For example, if you’re referencing a .NET Framework library that uses System.Directory­Services, it will fail if you run it on .NET Core 2.0 today, because System.DirectoryServices isn’t included in .NET Core. The Windows Compatibility Pack helps to extend the set of APIs covered by the compatibility mode by bringing in System.DirectoryServices.

Plan Your Migration

Unless your project is very small, you should take the time to plan your migration, and the most important part is understanding what you want to get out of it. Moving to .NET Core just because it’s the new hotness isn’t a good enough reason (unless you’re a true fan) as migrations are never free.

Let’s look at a typical .NET app, the Fabrikam Asset Management application. It consists of a Windows Presentation Foundation (WPF) front end and a Web API back end, storing data in a SQL Server, all deployed on Windows and on-premises. Fabrikam decided it would like to move its back end to Azure. The company is quite happy with the desktop application and wants to continue to leverage WPF. It also decided it’s best to use ASP.NET Core for the Web API back end as this allows more flexibility in the choice of server OS, as well as for isolated deployments using Docker.

A migration plan should include several steps instead of doing everything in one big swoop (also known as the big outage). This ensures you can deliver incremental value, keep your system operational, and learn and adapt as you perform the migration. Fabrikam’s migration plan looks like what’s shown in Figure 1.

Migrating a Typical .NET App Partially to the Cloud
Figure 1 Migrating a Typical .NET App Partially to the Cloud

As a first step, the plan moves the Web API back end to .NET Core, but the app will remain on Windows. This minimizes the amount of change the code must accommodate. The next step entails moving the .NET Core back end to the cloud. Then the company plans to move the back end to Linux. Later steps might involve Fabrikam deciding to leverage Docker.

The important point is: This is a step-by-step process and you want to make sure your application is operational after performing each step. This includes having the code compile and passing all tests (you do have tests, right?), but it might also mean being able to deploy the current system to production. Whether you need that is a function of how much time the migration will take and how much your system is under active development. Alternatively, you might decide to bring up the new system independently of the old one to reduce the risk of operational disruption in case you manage to corner yourself.

Understanding your goals and migration path helps to reveal any new constraints you need to factor in when refining your architecture. In this case, Fabrikam needs to share business logic and infrastructure code between the WPF application and the Web API back end. In the current system, both are running on top of .NET Framework so that code is simply contained in a class library that also targets .NET Framework. Moving forward, Fabrikam needs to share that code between .NET Framework and .NET Core, so it decides to give .NET Standard 2.0 a shot, as shown in Figure 2.

Handling Shared Code When Targeting Multiple .NET Implementations
Figure 2 Handling Shared Code When Targeting Multiple .NET Implementations

Once you understand which part of your existing code needs to be ported and what it needs to be ported to, you should use the API Port tool (which you’ll find at aka.ms/apiport). See my demo at bit.ly/2zkaKDn to understand how easy or difficult this might be. API Port scans your existing application binaries, including any third-party code you might have, and produces a report that shows you assembly by assembly how portable each is, and provides a table of all the APIs that are either unavailable or must be migrated. API Port isn’t specific to .NET Standard or .NET Core: You can select any .NET implementation you want, including .NET Framework, .NET Core, .NET Standard, UWP, Mono and Xamarin. This allows you to plan your migrations regardless of what you’re porting from and what you need to port to.

When you run API Port, I recommend you use the targets .NET Standard + Platform Extensions and .NET Core + Platform Extensions. Including the extensions ensures you don’t get false negatives for APIs that don’t ship as part of the platform, but can be added by referencing an additional NuGet package. I also recommend you use the command-line version of API Port and run it over your existing application as this is much easier, especially when your source code is spread across several solutions. This also allows you to assess third-party dependencies.

In the Fabrikam case, however, the company only needs to run API Port over the shared library:

$ apiport analyze -f C:\src\fabrikam\bin\Fabrikam.Shared.dll 
 -t ".NET Standard + Platform Extensions"

Fortunately for Fabrikam, the report API Port produces shows its library only uses APIs that are available for .NET Standard 2.0 (see Figure 3).

API Port Results for Fabrikam.Shared
Figure 3 API Port Results for Fabrikam.Shared

What’s in the Compatibility Pack?

The Windows Compatibility Pack is represented by the Microsoft.Windows.Compatibility NuGet package and contains about 40 components (see Figure 4 for the full list).

Figure 4 Available and Upcoming Components in the Windows Compatibility Pack

Component Status Windows-Only
Microsoft.Win32.Registry Available Yes
Microsoft.Win32.Registry.AccessControl Available Yes
System.CodeDom Available  
System.ComponentModel.Composition Coming  
System.Configuration.ConfigurationManager Available  
System.Data.DatasetExtensions Coming  
System.Data.Odbc Coming  
System.Data.SqlClient Available  
System.Diagnostics.EventLog Coming Yes
System.Diagnostics.PerformanceCounter Coming Yes
System.DirectoryServices Coming Yes
System.DirectoryServices.AccountManagement Coming Yes
System.DirectoryServices.Protocols Coming  
System.Drawing Coming  
System.Drawing.Common Available  
System.IO.FileSystem.AccessControl Available Yes
System.IO.Packaging Available  
System.IO.Pipes.AccessControl Available Yes
System.IO.Ports Available Yes

 

Component Status Windows-Only
System.Management Coming Yes
System.Runtime.Caching Coming  
System.Security.AccessControl Available Yes
System.Security.Cryptography.Cng Available Yes
System.Security.Cryptography.Pkcs Available Yes
System.Security.Cryptography.ProtectedData Available Yes
System.Security.Cryptography.Xml Available Yes
System.Security.Permissions Available  
System.Security.Principal.Windows Available Yes
System.ServiceModel.Duplex Available  
System.ServiceModel.Http Available  
System.ServiceModel.NetTcp Available  
System.ServiceModel.Primitives Available  
System.ServiceModel.Security Available  
System.ServiceModel.Syndication Coming  
System.ServiceProcess.ServiceBase Coming Yes
System.ServiceProcess.ServiceController Available Yes
System.Text.Encoding.CodePages Available Yes
System.Threading.AccessControl Available Yes

Given that .NET Framework was designed for Windows, about half of the compatibility pack is Windows-only as it depends on or wraps Windows technologies. But, as discussed earlier, the first step in migrating an existing .NET Framework code base should be to move to .NET Core but remain on Windows. For that step, not being able to use Windows-only technologies would just be a migration hurdle with zero architectural benefit.

If you want to migrate to Linux, the next step would be to assess how many technologies you’re depending on that work only on Windows. You’ll either replace those with cross-platform technologies (for example, replacing EventLog with TraceSource), separate them (for example, separating ActiveDirectory lookup logic from your authentication and authorization code), or guard the calls behind a platform check (for example, reading the registry only if you’re on Windows).

To assist in that process, we’ve designed Microsoft.Windows.Compatibility as a meta package, which means it doesn’t contain any libraries itself but simply references other NuGet packages. This allows you to quickly add references to the full list of components without having to install dozens of packages. However, as you’re migrating and removing dependencies on legacy technologies, it can be useful to reference only the components you need and remove the ones you’ve migrated away from to make sure new code doesn’t take a dependency on them again.

Using the Compatibility Pack

Let’s look further at Fabrikam’s Asset Management system. The infrastructure and business logic component needs to be shared between .NET Framework and .NET Core, which is why the company plans to convert it to target .NET Standard. After converting the project, the code no longer compiles because it uses APIs that aren’t part of .NET Standard.

In this case, the logging component reads settings from the registry to determine where the logs should be placed:

private static string GetLoggingPath()
{
  using (var key = Registry.CurrentUser.OpenSubKey(
    @"Software\Fabrikam\AssetManagement"))
  {
    if (key?.GetValue("LoggingDirectoryPath") is string configuredPath)
      return configuredPath;
  }
  var appDataPath = Environment.GetFolderPath(
    Environment.SpecialFolder.LocalApplicationData);
  return Path.Combine(appDataPath, "Fabrikam", "AssetManagement", "Logging");
}

That’s because the WPF desktop application happens to store its settings in the registry. Ideally, Fabrikam would refactor the application and would no longer store settings in the registry, but it would like to do this as a separate work item and not block the migration. The easiest way to get unblocked is by adding the compatibility pack. To do this, the company installs Microsoft.Windows.Compatibility. After the package is installed, the code compiles again. Fabrikam validates that both the WPF desktop application and the Web API continue to work with the newly created .NET Standard library.

At this point, you might wonder what happens if this code runs on Linux. So, let’s do a fast-forward and see what happens when Fabrikam completes the migration of the Web API back end to .NET Core and starts to migrate from Windows to Linux. During startup, the Web API app configures, among other things, the logging infrastructure. As you might expect, that doesn’t work so well because it calls into the registry to determine the path where the logs should be placed. And, sure enough, the Web API crashes on startup with an exception, as shown in Figure 5.

Figure 5 Looking for the Registry, the Web API Crashes on Startup

Unhandled Exception: System.TypeInitializationException: The type initializer for 
'Microsoft.Win32.Registry' threw an exception. ---> System.PlatformNotSupportedException: 
Registry is not supported on this platform.
  at Microsoft.Win32.RegistryKey.OpenBaseKeyCore(RegistryHive hKey,
    RegistryView view)
  at Microsoft.Win32.Registry..cctor()
  --- End of inner exception stack trace ---
  at Fabrikam.Infrastructure.Logging.Logger.GetLoggingPath()
  at Microsoft.AspNetCore.Hosting.Internal.WebHost.EnsureStartup()
  at Microsoft.AspNetCore.Hosting.Internal.WebHost.EnsureApplicationServices()
  at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
  at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
  at Fabrikam.AssetManagement.WebApi.Program.BuildWebHost(String[] args)
  at Fabrikam.AssetManagement.WebApi.Program.Main(String[] args))

The fix for this is simple: Fabrikam needs to guard the registry call with a platform check, as shown in Figure 6.

Figure 6 Guarding the Registry Call

private static string GetLoggingPath()
{
  if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  {
    using (var key = Registry.CurrentUser.OpenSubKey(
      @"Software\Fabrikam\AssetManagement"))
    {
      if (key?.GetValue("LoggingDirectoryPath") is string configuredPath)
        return configuredPath;
    }
  }
  var appDataPath = Environment.GetFolderPath(
    Environment.SpecialFolder.LocalApplicationData);
  return Path.Combine(appDataPath, "Fabrikam", "AssetManagement", "Logging");
}

In this case, the fix was straightforward as the code already handled the situation where the registry didn’t contain a configuration for the logging path. In general, you need to decide what the fallback logic is when you can’t call a Windows-only API. This might mean using a Linux-specific API, replacing the Windows-only concept with one that’s cross-­platform, or simply doing nothing. The latter can make sense if you just want to provide some extra value for customers who are running on Windows, as in this case.

Understanding Windows-Only Dependencies

Something tells me that reading the last paragraphs made you slightly uneasy. I bet you’re wondering how you can use the Compatibility Pack safely in code you plan to migrate later or in code you know will need to run on Linux. You might even go so far as to conclude that this whole idea is diluting the promise of .NET Standard, which is about building libraries that can work on any .NET implementation and on any OS.

We don’t think it’s that dire because:

Being able to call non-portable APIs is convenient and powerful. At first, it’s tempting to think you’re better off if these APIs wouldn’t be available at all. But consider the example of Fabrikam’s shared library: It’s much easier to just call the API under a guard than it is having to split, especially if your library needs just a very few Windows-only APIs. Of course, it might still make sense to refactor your code to not depend on platform-specific APIs—it just means you have the choice not to, if only temporarily.

Non-portable APIs aren’t available by default. Having many Windows-only APIs is the primary reason why the APIs in Compatibility Pack aren’t part of .NET Standard. We want to make sure that what .NET Standard offers by default works everywhere.

Tooling warns when non-portable APIs are used. We provide an API Analyzer (bit.ly/2iV9n7o) that detects usage of non-­portable APIs and warns you as you’re developing your code.

Let’s see how API Analyzer would work in the context of Fabrikam’s shared library. The analyzer is provided in the NuGet package Microsoft.DotNet.Analyzers.Compatibility. After it’s installed, the error list shows warnings in the GetLoggingPath method, as shown in Figure 7.

API Analyzer Warns When Non-Portable APIs Are Used
Figure 7 API Analyzer Warns When Non-Portable APIs Are Used

It informs you that both OpenSubKey and GetValue aren’t available and will throw PlatformNotSupportedException on Linux and macOS. It’s worth pointing out that this analyzer is powered by Roslyn, which means it provides immediate feedback and doesn’t require you to compile. This is very powerful as you’re getting information as you’re developing your code, so you can focus on the task at hand without having to remember to run a tool after the fact.

To address the warning, you’ll need to add the platform guard and then suppress the warning, as shown in Figure 8.

Suppressing Warnings for Use of Non-Portable APIs
Figure 8 Suppressing Warnings for Use of Non-Portable APIs

My preference is to use the global suppression file (which will suppress this warning for the method you’re currently in but records that suppression in a secondary file) as opposed to using the in-source option (which will put a #pragma suppression around the current statement), but the choice is yours. While the analyzer provides a great interactive experience in Visual Studio, it’s by no means tied to Visual Studio or to the concept of an IDE. Analyzers are run by the compiler. If you checked in the code and submitted it to your Linux CI server or just compiled it from the command line, you’d get the same warnings:

$ dotnet build
Logger.cs(17,43): warning PC001: 
  RegistryKey.OpenSubKey(string) isn't supported on Linux, MacOSX
Logger.cs(19,18): warning PC001: 
  RegistryKey.GetValue(string) isn't supported on Linux, MacOSX
  Fabrikam.Shared -> /home/immol/Fabrikam.Shared/bin/Debug/netstandard2.0/
Fabrikam.Shared.dll
Build succeeded.

The analyzer is also configurable as to whether these are mere suggestions, warnings or even errors. The default is warnings, but, as Figure 9 shows, you can configure them to be errors, too.

Configuring the Severity Level for Non-Portable APIs
Figure 9 Configuring the Severity Level for Non-Portable APIs

Wrapping Up

Each migration is different. Before porting to .NET Core, you should understand your goals and what you want the migration to accomplish. Use the API Port tool to assess how portable your existing code is.

Plan your migration not as a single monolithic operation but rather as a series of steps along the migration path. Don’t assume you can port an existing application all at once. This allows you to realize value as you go and learn more about potential issues and how your architecture needs to change to become cross-platform.

Take advantage of the Windows Compatibility Pack, which provides access to about 40 .NET Framework components and can be referenced from both .NET Core and .NET Standard projects. However, definitely keep in mind that this package includes Windows-only components. If you plan to go cross-platform, use the API Analyzer, which will inform you whenever you’re using non-portable APIs. Guard the calls to these APIs accordingly and provide sensible fallback logic, then suppress the warning.

And, last, keep in mind that not porting is also an option. The .NET Framework is still the best choice for certain kinds of apps, particularly desktop applications.


Immo Landwerth is a program manager working on the .NET platform team at Microsoft. He specializes in the base class library, API design and .NET Standard.

Thanks to the following Microsoft technical experts who reviewed this article: Wes Haggard and Dan Moseley
Dan Moseley is a software engineering manager and Wes Haggard is a software engineer. Both work with Immo on the .NET team at Microsoft.


Discuss this article in the MSDN Magazine forum