Migrating a Sample WPF App to .NET Core 3 (Part 2)
In part 1 of this blog series, I began the process of porting a sample WPF app to .NET Core. In that post, I described the .NET Core migration process as having four steps:
We previously went through the first two steps – reviewing the app and its dependencies (including NuGet dependencies and a .NET Portability Analyzer report), updating NuGet package references, and migrating the project file. In this post, we’ll complete the migration by making the necessary code changes to get the app building and running against .NET Core 3.
Step 3: Fix build issues
The third step of the porting process is getting the project to build. If you try to run
dotnet build on the sample project now (or build it in VS), there will be about 100 errors, but we’ll get them fixed up quickly.
System.ServiceModel references and Microsoft.Windows.Compatibility
The majority of the errors are due to missing System.ServiceModel types. These can be easily addressed by referencing the necessary WCF NuGet packages. An even better solution, though, is to use the Microsoft.Windows.Compatibility package. This metapackage includes a wide variety of .NET packages that work on .NET Core but that don’t necessarily work cross-platform. The APIs in the compatibility pack include APIs relating to WCF client, directory services, registry, configuration, ACLs, and more.
In most .NET Core 3 WPF and WinForms porting scenarios, it will be useful to reference the Microsoft.Windows.Compatibility package preemptively since it includes a broad set of APIs that are common in WPF and WinForms .NET Framework apps.
After adding the NuGet reference to Microsoft.Windows.Compatibility, only one build error remains!
Cleaning up unused files
The next build error we see in the sample refers to a bad interface implementation in OldUnusedViewModel.cs. The file name is a hint, but on inspecting we find that, in fact, this source file is incorrect. It didn’t cause issues previously because it wasn’t included in the original .NET Framework project. This sort of issue comes up frequently when migrating to .NET Core since SDK-style projects include all C# (and XAML) sources by default. Source files that were present on disk but not included in the old csproj get included now automatically.
For one-off issues like this, it’s easy to compare to the previous csproj to confirm that the file isn’t needed and then either
<Compile Remove="" /> it or, if the source file isn’t needed anywhere anymore, delete it. In this case, it’s safe to just delete OldUnusedViewModel.cs.
If you have many source files that would need to be excluded this way, you can disable auto-inclusion of C# files by setting the
<EnableDefaultCompileItems> property to false in the project file. Then, you can copy
<Compile Include> items from the old project file to the new one in order to only build sources you intended to include. Similarly,
<EnableDefaultPageItems> can be used to turn off auto-inclusion of XAML pages and
<EnableDefaultItems> can control both with a single property.
A brief aside on multi-pass compilers
After removing the offending file, we re-build and get five errors. Didn’t we use to have one? Why did the number of errors go up? The C# compiler is a multi-pass compiler. This means that it goes through each source file twice* – first the compiler just looks at metadata and declarations in each source file and identifies any declaration-level problems. Those are the errors we’ve just fixed. Then, it goes through the code again to build the C# source into IL (those are this second set of errors that we’re seeing now).
* In reality, the C# compiler does more than just two passes (as explained in Eric Lippert’s blog on the topic), but the end result is that compiler errors for large code changes like this tend to come in two waves.
Third-party dependency fix-ups (Castle.Windsor)
The next set of errors we see are related to Castle.Windsor APIs. This may seem surprising since the .NET Core Bean Trader project is using the same version of Castle.Windsor as the .NET Framework-targeted project (4.1.1). The differences are because within a single NuGet package, there can be different libraries for use with different .NET targets. This allows the packages to support many different .NET platforms which may require different implementations. It also means that there may be small API differences in the libraries when targeting different .NET platforms.
In the case of the BeanTrader sample, we see the following issues that need fixed up:
Castle.MicroKernel.Registration.Classes.FromThisAssemblyis not available on .NET Core. There is, however, the very similar API
Classes.FromAssemblyContainingavailable, so we can replace both uses of
Classes.FromThisAssembly()with calls to
tis the type making the call.
- Similarly, in Bootstrapper.cs,
Castle.Windsor.Installer.FromAssembly.Thisis unavailable on .NET Core. Instead, that call can be replaced with
Updating WCF client usage
Having fixed the Castle.Windsor differences, the last remaining build errors in the .NET Core project are that
BeanTraderServiceClient (which derives from
DuplexClientBase) does not have
Close methods. This is not surprising since these are the missing APIs that were highlighted by the .NET Portability Analzyer at the beginning of this migration process.
BeanTraderServiceClient draws our attention to a larger issue, though. This WCF client was auto-generated by the Svcutil.exe tool.
WCF clients generated by Svcutil are only meant for use on .NET Framework. Solutions that use svcutil-generated WCF clients will need to regenerate .NET Standard-compatible clients for use with .NET Core. One of the main reasons the old clients won’t work is that they depend on app configuration for defining WCF bindings and endpoints. Because .NET Standard WCF APIs can work cross-platform (where System.Configuration APIs aren’t available), WCF clients for .NET Core and .NET Standard scenarios must define bindings and endpoints programmatically instead of in configuration.
In fact, any WCF client usage that depends on the
<system.serviceModel> app.config section (whether created with Svcutil or manually) will need changed to work on .NET Core.
There are two ways to automatically generate .NET Standard-compatible WCF clients:
- The dotnet-svcutil tool is a .NET Core CLI tool that generates WCF clients similar to how Svcutil worked previously.
- Visual Studio can generate WCF clients using the WCF Web Service Reference option of its Connected Services feature.
Either approach works well. Alternatively, of course, you could write the WCF client code yourself. For this sample, I chose to use the Visual Studio Connected Service feature. To do that, right click on the BeanTraderClient.Core project in Visual Studio’s solution explorer and select Add -> Connected Service. Next, choose the WCF Web Service Reference Provider. This will bring up a dialog where you can specify the address of the backend Bean Trader web service and the namespace that generated types should use.
After clicking the Finish button, a new ‘Connected Services’ node is added to the project and a Reference.cs file is added under that node containing the new .NET Standard WCF client for accessing the Bean Trader service. If you look at the
GetBindingForEndpoint methods in that file, you will see that bindings and endpoints are now generated programmatically (instead of via app config).
Our project has new WCF client classes now (in Reference.cs), but it also still has the old ones (in BeanTrader.cs). There are two options at this point:
- If you don’t want to make any changes to the original .NET Framework version of the app, you can use a
<Compile Remove="BeanTrader.cs" />item in the .NET Core project’s csproj file so that the .NET Framework and .NET Core versions of the app use different WCF clients. This has the advantage of leaving the existing .NET Framework project unchanged, but has the disadvantage that code using the generated WCF clients may need to be slightly different in the .NET Core case than it was in the .NET Framework project, so you will likely need to use
#ifdirectives to conditionally compile some WCF client usage (creating clients, for example) to work one way when built for .NET Core and another way when built for .NET Framework.
- If, on the other hand, some code churn in the existing .NET Framework project is acceptable, you can remove BeanTrader.cs all together and add Reference.cs to the .NET Framework project. Because the new WCF client is built for .NET Standard, it will work in both .NET Core and .NET Framework scenarios. This approach has the advantage that the code won’t need to bifurcate to support two different WCF clients – the same code will be used everywhere. The drawback, of course, is that it involves changing the (presumably stable) .NET Framework project.
In the case of the Bean Trader sample, we can make small changes to the original project if it makes migration easier, so follow these steps to reconcile WCF client usage:
- Add the new Reference.cs file to the .NET Framework BeanTraderClient.csproj project using the ‘Add existing item’ context menu from the solution explorer. Be sure to add ‘as link’ so that the same file is used by both projects (as opposed to copying the C# file).
- Delete BeanTrader.cs from the BeanTraderClient.csproj project (which will also remove it from the .NET Core project).
- The new WCF client is very similar to the old one, but a number of namespaces in the generated code are different. Because of this, it is necessary to update the project so that WCF client types are used from BeanTrader.Service instead of BeanTrader.Model or without a namespace). Building BeanTraderClient.Core.csproj will help to identify where these changes need to be made. Fixes will be needed both in C# and in XAML source files.
- Finally, you will discover that there is an error in BeanTraderServiceClientFactory.cs because the available constructors for the
BeanTraderServiceClienttype have changed in the new client. It used to be possible to supply an
InstanceContextargument (which we created using a
CallbackHandlerfrom the Castle.Windsor IoC container). The new constructors create new
CallbackHandlers instead, though. There are, however, constructors in
BeanTraderServiceClient‘s base type that match what we want. Since the auto-generated WCF client code all exists in partial classes, we can easily extend it. To do this, create a new file called BeanTraderServiceClient.cs (in the BeanTraderClient.csproj project so that it’s included in both the .NET Framework and the .NET Core proejcts) and add a partial class with that same name (using the BeanTrader.Service namespace). Then, add one constructor to the partial type as shown here:
With those changes made, we can create the WCF client instance with the same constructor as before and both projects will now be using a new .NET Standard-compatible WCF client. We can then change the
Open call in TradingService.cs to use
await OpenAsync, instead.
We also need to address the
Close call in the same file. Since the
Close method is called from a
Dispose method, it would be nice to have a non-async version of the method to call (even though calling the async alternative would be harmless in this case). Fortunately, newer versions of System.ServiceModel.Primitives now include
ClientBase<T>.Close. The latest stable version of the Microsoft.Windows.Compatibility package (as of the time of this blog post) includes version 4.4.1 of System.ServiceModel.Primitives, but by adding a direct package dependency on System.ServiceModel.Primitives version 4.5.3, it is possible to call
ClientBase<T>.Close in the trading service’s
Dispose method, as before.
With the WCF issues addressed, the .NET Core version of the Bean Trader sample now builds cleanly!
Making sure the .NET Framework project still builds
Before wrapping up the ‘build-time fixes’ step, there’s one more issue to address. Although BeanTraderClient.Core.csproj builds without errors, the original .NET Framework-targeted BeanTraderClient.csproj now has errors! The primary error is this:
Error: Your project does not reference ".NETFramework,Version=v4.7.2" framework. Add a reference to ".NETFramework,Version=v4.7.2" in the "TargetFrameworks" property of your project file and then re-run NuGet restore.
This is because both of the project files build to the same output and intermediate output paths and the .NET Core project’s project.assets.json file (generated by
dotnet restore) is conflicting with the .NET Framework build. If we only worked on one of these projects at a time, this could be avoided by just cleaning the obj/ and bin/ folders when switching projects, but in this scenario we have both projects open in Visual Studio together.
The solution is to update the projects’ output and intermediate output paths to something based on project name. A challenge here is that setting
<BaseIntermediateOutputPath> in the csproj files directly won’t work because SDK-style projects use the intermediate output path as part of the
Sdk="Microsoft.NET.Sdk.WindowsDesktop" declaration before we have an opportunity to change it in the csproj. This problem is discussed in more detail in Microsoft/msbuild#1603.
Instead, we can use a Directory.Build.props file to set the intermediate output path for both projects at once (and, importantly, prior to the intermediate output path being used by the project’s SDK). To do this, add a file called Directory.Build.props in the BeanTraderClient folder with the following contents:
That will update BaseOutputPath and BaseIntermediateOutputPath for both projects to be under directories based on their project name (out/$(MSBuildProjectName)).
Finally, because C# files are generated in intermediate output paths and we don’t want the .NET Core project to compile the .NET Framework project’s temporary files, we need to add
<Compile Remove="out\BeanTraderClient\**\*.cs" /> to BeanTraderClient.Core.csproj.
The solution (including both .NET Framework and .NET Core versions of the Bean Trader app) now builds successfully!
Step 4: Runtime testing
It’s easy to forget that migration work isn’t done as soon as the project builds cleanly against .NET Core. It’s important to leave time for testing the ported app, too.
Let’s try launching the newly-ported Bean Trader app and see what happens. The app doesn’t get very far before failing with the following exception:
System.Configuration.ConfigurationErrorsException: 'Configuration system failed to initialize'
ConfigurationErrorsException: Unrecognized configuration section system.serviceModel.
This makes sense, of course. Remember that WCF no longer uses app configuration, so the old system.serviceModel section of the app.config file needs to be removed. The updated WCF client includes all of the same information in its code, so the config section isn’t needed anymore.
After removing the system.serviceModel section of app.config, the app launches but fails with another exception when a user signs in:
System.PlatformNotSupportedException: 'Operation is not supported on this platform.'
The unsupported API is
Func<T>.BeginInvoke. As explained in dotnet/corefx#5940, .NET Core doesn’t support the
EndInvoke methods on delegate types due to underlying remoting dependencies. I explained this issue (and its fix) in more detail in a previous blog post, but the gist is that
EndInvoke calls should be replaced with
Task.Run (or async alternatives, if possible). Applying the general solution here, the
BeginInvoke call can be replaced with an
Invoke call launched by
After removing the
BeginInvoke usage, the Bean Trader app runs successfully on .NET Core!
All apps are different, of course, so the specific steps needed to migrate your own apps to .NET Core will vary. But I hope this example demonstrates the general workflow and the types of issues that can be expected. And, despite these posts’ length, the actual changes needed in the Bean Trader sample to make it work on .NET Core were fairly limited. Many apps migrate to .NET Core in this same way – with limited or even no code changes needed.