Migrating WPF apps to .NET Core
This article covers the steps necessary to migrate a Windows Presentation Foundation (WPF) app from .NET Framework to .NET Core 3.0. If you don't have a WPF app on hand to port, but would like to try out the process, you can use the Bean Trader sample app available on GitHub. The original app (targeting .NET Framework 4.7.2) is available in the NetFx\BeanTraderClient folder. First we'll explain the steps necessary to port apps in general, and then we'll walk through the specific changes that apply to the Bean Trader sample.
Important
The Desktop Guide documentation for .NET 6 and .NET 5 (including .NET Core 3.1) is under construction.
Try the upgrade assistant
The .NET Upgrade Assistant is a command-line tool that can be run on different kinds of .NET Framework apps. It's designed to assist with upgrading .NET Framework apps to .NET 5. After running the tool, in most cases the app will require additional effort to complete the migration. The tool includes the installation of analyzers that can assist with completing the migration.
For more information, see Upgrade a Windows Forms App to .NET 5 with the .NET Upgrade Assistant.
Prerequisites
To migrate to .NET Core, you must first:
Understand and update NuGet dependencies:
- Upgrade NuGet dependencies to use the
<PackageReference>format.
- Review top-level NuGet dependencies for .NET Core or .NET Standard compatibility.
- Upgrade NuGet packages to newer versions.
- Use the .NET Portability Analyzer to understand .NET dependencies.
Migrate the project file to the new SDK-style format:
- Choose whether to target both .NET Core and .NET Framework, or only .NET Core.
- Copy relevant project file properties and items to the new project file.
Fix build issues:
- Add a reference to the Microsoft.Windows.Compatibility package.
- Find and fix API-level differences.
- Remove app.config sections other than
appSettingsor
connectionStrings.
- Regenerate generated code, if necessary.
Runtime testing:
- Confirm the ported app works as expected.
- Beware of NotSupportedException exceptions.
About the sample
This article references the Bean Trader sample app because it uses a variety of dependencies similar to those that real-world WPF apps might have. The app isn't large, but is meant to be a step up from 'Hello World' in terms of complexity. The app demonstrates some issues users may encounter while porting real apps. The app communicates with a WCF service, so for it to run properly, you'll also need to run the BeanTraderServer project (available in the same GitHub repository) and make sure the BeanTraderClient configuration points to the correct endpoint. (By default, the sample assumes the server is running on the same machine at, which will be true if you launch BeanTraderServer locally.)
Keep in mind that this sample app is meant to demonstrate .NET Core porting challenges and solutions. It's not meant to demonstrate WPF best practices. In fact, it deliberately includes some anti-patterns to make sure you come across at least a couple of interesting challenges while porting.
Getting ready
The primary challenge of migrating a .NET Framework app to .NET Core is that its dependencies may work differently or not at all. Migration is much easier than it used to be; many NuGet packages now target .NET Standard. Starting with .NET Core 2.0, the .NET Framework and .NET Core surface areas have become similar. Even so, some differences (both in support from NuGet packages and in available .NET APIs) remain. The first step in migrating is to review the app's dependencies and make sure references are in a format that's easily migrated to .NET Core.
Upgrade to
<PackageReference> NuGet references
Older .NET Framework projects typically list their NuGet dependencies in a packages.config file. The new SDK-style project file format references NuGet packages as
<PackageReference> elements in the csproj file itself rather than in a separate config file.
When migrating, there are two advantages to using
<PackageReference>-style references:
- This is the style of NuGet reference that determine what dependencies your project has, which is useful when determining whether the necessary dependencies will work on .NET Core or not.
The first step to migrating a .NET Framework app to .NET Core is to update it to use
<PackageReference> NuGet references. Visual Studio makes this simple. Just right-click the project's packages.config file in Visual Studio's Solution Explorer, and then select Migrate packages.config to PackageReference.
A dialog appears showing calculated top-level NuGet dependencies and asking which other NuGet packages should be promoted to top-level. None of these other packages need to be top-level for the Bean Trader sample, so you can uncheck all of those boxes. Then, click Ok and the packages.config file is removed and
<PackageReference> elements are added to the project file.
<PackageReference>-style references don't store NuGet packages locally in a packages folder. Instead, they're stored globally as an optimization. After the migration completes, edit the csproj file and remove any
<Analyzer> elements referring to the analyzers that previously came from the ..\packages directory. Don't worry; since you still have the NuGet package references, the analyzers will be included in the project. You just need to clean up the old packages.config-style
<Analyzer> elements.
Review NuGet packages
Now that you can see the top-level NuGet packages that the project depends on, you can review whether those packages are available on .NET Core. You can determine whether a package supports .NET Core by looking at its dependencies on nuget.org. The community-created fuget.org site shows this information prominently at the top of the package information page.
When targeting .NET Core 3.0, any packages targeting .NET Core or .NET Standard should work (since .NET Core implements the .NET Standard surface area). In some cases, the specific version of a package that's used won't target .NET Core or .NET Standard, but newer versions will. In this case, you should consider upgrading to the latest version of the package.
You can use packages targeting .NET Framework, as well, but that introduces some risk. .NET Core to .NET Framework dependencies are allowed because .NET Core and .NET Framework surface areas are similar enough that such dependencies often work. However, if the package tries to use a .NET API that isn't present in .NET Core, you'll encounter a runtime exception. Because of that, you should only reference .NET Framework packages when no other options are available and understand that doing so imposes a test burden.
If there are packages referenced that don't target .NET Core or .NET Standard, you'll have to think about other alternatives:
- Are there other similar packages that can be used instead? Sometimes NuGet authors publish separate '.Core' versions of their libraries specifically targeting .NET Core. Enterprise Library packages are an example of the community publishing ".NetCore" alternatives. In other cases, newer SDKs for a particular service (sometimes with different package names) are available for .NET Standard. If no alternatives are available, you can proceed using the .NET Framework-targeted packages, bearing in mind that you'll need to test them thoroughly once running on .NET Core.
The Bean Trader sample has the following top-level NuGet dependencies:
Castle.Windsor, version 4.1.1
This package targets .NET Standard 1.6, so it works doesn't target .NET Core, but the newer 5.0 version does. This is common when migrating because many NuGet packages have added .NET Standard support recently, but older project versions will only target .NET Framework. If the version difference is only a minor version difference, it's often easy to upgrade to the newer version. Because this is a major version change, you need to be cautious upgrading since there could be breaking changes in the package. There is a path forward, though, which is good.
MahApps.Metro, version 1.6.5
This package also doesn't target .NET Core, but has a newer pre-release (2.0-alpha) that does. Again, you have to look out for breaking changes, but the newer package is encouraging.
The Bean Trader sample's NuGet dependencies all either target .NET Standard/.NET Core or have newer versions that do, so there are unlikely to be any blocking issues here.
Upgrade NuGet packages
If possible, it would be good to upgrade the versions of any packages that only target .NET Core or .NET Standard with more recent versions at this point (with the project still targeting .NET Framework) to discover and address any breaking changes early.
If you would rather not make any material changes to the existing .NET Framework version of the app, this can wait until you have a new project file targeting .NET Core. However, upgrading the NuGet packages to .NET Core-compatible versions ahead of time makes the migration process even easier once you create the new project file and reduces the number of differences between the .NET Framework and .NET Core versions of the app., however, that may not be feasible. In these cases, don't upgrade MahApps.Metro because the necessary changes are non-trivial and this tutorial focuses, it would be good to.
<ItemGroup> <PackageReference Include="Castle.Windsor"> <Version>4.1.1</Version> </PackageReference> <PackageReference Include="MahApps.Metro"> <Version>1.6.5</Version> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers"> <Version>2.9.2</Version> </PackageReference> <PackageReference Include="Nito.AsyncEx"> <Version>5.0.0</Version> </PackageReference> </ItemGroup>
.NET Framework portability analysis
Once you understand the state of your project's NuGet dependencies, the next thing to consider is . You can read more about using the .NET Portability Analyzer (API Port) using the GUI in the Porting desktop apps to .NET Core blog post. If you prefer to use the command line, the necessary steps are:
Download the .NET Portability Analyzer if you don't already have it.
Make sure the .NET Framework app to be ported builds successfully (this is a good idea prior to migration regardless).. The
-targument specifies which .NET platform to analyze API usage against. In this case, you want .NET Core.
When you open the HTML report, the first section will list all of the analyzed binaries and what percentage of the .NET APIs they use are available on the targeted platform. The percentage is not meaningful by itself. What's more useful is to see the specific APIs that are missing. To do that, either select an assembly name or scroll down to the reports for individual assemblies.
Focus on the assemblies that you own the source code for. In the Bean Trader ApiPort report, for example, there are many binaries listed, but most of them belong to NuGet packages.
Castle.Windsor shows that it depends on some System.Web APIs that are missing in .NET Core. This isn't a concern because).
With the Bean Trader sample, the only binary that's .NET Core surface area (using), you see that there are async alternatives in .NET Core instead.
Based on this report and the previous NuGet dependency analysis, it looks like there should be no major issues migrating the Bean Trader sample to .NET Core. You're ready for the next step in which you'll actually start the migration.
Migrating the project file
If your app isn't using the new SDK-style project file format, you'll need a new project file to target .NET Core. You can replace the existing csproj file or, if you prefer to keep the existing project untouched in its current state, you can add a new csproj file targeting .NET Core. You can build versions of the app for .NET Framework and .NET Core with a single SDK-style project file with multi-targeting (specifying multiple
<TargetFrameworks> targets).
To create the new project file, you can create a new WPF project in Visual Studio or use the
dotnet new wpf command in a temporary directory to generate the project file and then copy/rename it to the correct location. There is also a community-created tool, CsprojToVs2017, that can automate some of the project file migration. top-level ones. If you have already migrated to
<PackageReference> elements with Visual Studio (as you've done in this sample), then the tool can help with the rest of the conversion. Like Scott Hanselman recommends in his blog post on migrating csproj files, porting by hand is educational and will give better results if you only have a few projects to port. But if you're porting dozens or hundreds of project files, then a tool like CsprojToVs2017 can be a help.
To create a new project file for the Bean Trader sample,, open the old and new project files side-by-side and look through the old one to see if any information it contains needs to be migrated. In the Bean Trader sample case, the following items should be copied to the new project:
The
<RootNamespace>,
<AssemblyName>, and
<ApplicationIcon>properties should all be copied.
You also need to add a
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>property to the new project file since the Bean Trader sample includes assembly-level attributes (like
[AssemblyTitle]) in an AssemblyInfo.cs file. By default, new SDK-style projects will autogenerate these attributes based on properties in the csproj file. Because you don't want that to happen in this case (the autogenerated attributes would conflict with those from AssemblyInfo.cs), you disable the autogenerated attributes with
<GenerateAssemblyInfo>.
Although resx files are automatically included as embedded resources, other
<Resource>items like images are not. So, copy the
<Resource>elements for embedding image and icon files. You can simplify the png references to a single line by using the new project file format's support for globbing patterns:
<Resource Include="**\*.png" />.
Similarly,
<None>items are included automatically, but they aren't copied to the output directory, by default. Because the Bean Trader project includes a
<None>item that is copied to the output directory (using
PreserveNewestbehaviors), you need to update the automatically populated
<None>item for that file, like this.
<None Update="BeanTrader.pfx"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None>>, however, since it's a XAML file. So, you need to both remove the XAML file as a page (
<Page Remove="**\Default.Accent.xaml" />) and add it as content.
<Content Include="Resources\Themes\Default.Accent.xaml"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content>
Finally, add NuGet references by copying the
<ItemGroup>with all the
<PackageReference>elements. If you hadn't previously upgraded the NuGet packages to .NET Core-compatible versions, you could do that now that the package references are in a .NET Core-specific project.
At this point, it should be possible to add the new project to the BeanTrader solution and open it in Visual Studio. The project should look correct in Solution Explorer, and
dotnet restore BeanTraderClient.Core.csproj should successfully restore packages (with two expected warnings related to the MahApps.Metro version you're using targeting .NET Framework).
Although it's possible to keep both project files side-by-side (and may even be desirable if you want to keep building the old project exactly as it was), it complicates the migration process (the two projects will try to use the same bin and obj folders) and usually isn't necessary. If you want to build for both .NET Core and .NET Framework targets, you can replace the
<TargetFramework>netcoreapp3.0</TargetFramework> property in the new project file with
<TargetFrameworks>netcoreapp3.0;net472</TargetFrameworks> instead. For the Bean Trader sample, delete the old project file (BeanTraderClient.csproj) since it's no longer needed. If you prefer to keep both project files, be sure to have them build to different output and intermediate output paths.
Fix build issues
The third step of the porting process is getting the project to build. Some apps will already build successfully once the project file is converted to an SDK-style project. If that's the case for your app, congratulations! You can go on to Step 4. Other apps will need some updates to get them building for .NET Core. If you try to run
dotnet build on the Bean Trader sample project now, for example, (or build it in Visual Studio), there will be many errors, but you'll get them fixed quickly.
System.ServiceModel references and Microsoft.Windows.Compatibility
A common source of errors is missing references for APIs that are available for .NET Core but not automatically included in the .NET Core app metapackage. To address this, you should reference the
Microsoft.Windows.Compatibility package. The compatibility package includes a broad set of APIs that are common in Windows desktop apps, such as WCF client, directory services, registry, configuration, ACLs APIs, and more.
With the Bean Trader sample, the majority of the build errors are due to missing System.ServiceModel types. These could be addressed by referencing the necessary WCF NuGet packages. WCF client APIs are among those present in the
Microsoft.Windows.Compatibility package, though, so referencing the compatibility package is an even better solution (since it also addresses any issues related to APIs as well as solutions to the WCF issues that the compatibility package makes available). The
Microsoft.Windows.Compatibility package helps in most .NET Core 3.0 WPF and WinForms porting scenarios. After adding the NuGet reference to
Microsoft.Windows.Compatibility, only one build error remains!
Cleaning up unused files
One type of migration issue that comes up often relates to C# and XAML files that weren't previously included in the build getting picked up by the new SDK-style projects that include all source automatically.
The next build error you see in the Bean Trader sample refers to a bad interface implementation in OldUnusedViewModel.cs. The file name is a hint, but on inspection, you'll find that this source file is incorrect. It didn't cause issues previously because it wasn't included in the original .NET Framework project. Source files that were present on disk but not included in the old csproj are included automatically now.
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 from the Bean Trader sample, you can re-build and will get four errors. Didn't you have one before? 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 you've fixed. Then it goes through the code again to build the C# source into IL; those are the second set of errors that you're seeing now.
Note
The C# compiler does more than just two passes, but the end result is that compiler errors for large code changes like this tend to come in two waves.
Third-party dependency fixes (Castle.Windsor)
Another class of issue that comes up in some migration scenarios is API differences between .NET Framework and .NET Core versions of dependencies. Even if a NuGet package targets both .NET Framework and .NET Standard or .NET Core, there may 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.
The next set of errors you'll see in the Bean Trader sample are related to
Castle.Windsor APIs. The .NET Core Bean Trader project uses the same version of
Castle.Windsor as the .NET Framework-targeted project (4.1.1), but the implementations for those two platforms are slightly different.
In this case, you see the following issues that need to be fixed:
Castle.MicroKernel.Registration.Classes.FromThisAssemblyisn't available on .NET Core. There is, however, the similar API
Classes.FromAssemblyContainingavailable, so we can replace both uses of
Classes.FromThisAssembly()with calls to
Classes.FromAssemblyContaining(t), where
tis the type making the call.
- Similarly, in Bootstrapper.cs,
Castle.Windsor.Installer.FromAssembly.This is unavailable on .NET Core. Instead, that call can be replaced with
FromAssembly.Containing(typeof(Bootstrapper)).
Updating WCF client usage
Having fixed the
Castle.Windsor differences, the last remaining build error in the .NET Core Bean Trader project is that
BeanTraderServiceClient (which derives from
DuplexClientBase) doesn't have an
Open method. This isn't surprising since this is an API that was highlighted by the .NET Portability Analyzer at the beginning of this migration process. Looking at
BeanTraderServiceClient draws our attention to a larger issue, though. This WCF client was autogenerated by the Svcutil.exe tool.
WCF clients generated by Svcutil are 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 to be changed to work on .NET Core.
There are two ways to automatically generate .NET Standard-compatible WCF clients:
- The
dotnet-svcutiltool is a .NET tool that generates WCF clients in a way that is 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 (
localhost:8080 if you are running the server locally) and the namespace that generated types should use (BeanTrader.Service, for example).
After you select 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
GetEndpointAddress or
GetBindingForEndpoint methods in that file, you'll see that bindings and endpoints are now generated programmatically (instead of via app config). The 'Add Connected Services' feature may also add references to some System.ServiceModel packages in the project file, which aren't needed since all necessary WCF packages are included via Microsoft.Windows.Compatibility. Check the csproj to see if any extra System.ServiceModel
<PackageReference> items have been added, and if so, remove them.
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 want to be able to build the original .NET Framework project (alongside the new .NET Core-targeted one), 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'll 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. Because the new WCF client is built for .NET Standard, it will work in both .NET Core and .NET Framework scenarios. If you are building for .NET Framework in addition to .NET Core (either by multi-targeting or by having two csproj files), you can use this new Reference.cs file for both targets. 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 is that it involves changing the (presumably stable) .NET Framework project.
In the case of the Bean Trader sample, you 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). If you are building for both .NET Core and .NET Framework with a single csproj (using multi-targeting) then this step isn't necessary.
Delete BeanTrader.cs.
The new WCF client is 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 (or whatever namespace name you chose) 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'll discover that there is an error in BeanTraderServiceClientFactory.cs because the available constructors for the
BeanTraderServiceClienttype have changed. It used to be possible to supply an
InstanceContextargument (which was created using a
CallbackHandlerfrom the
Castle.WindsorIoC container). The new constructors create new
CallbackHandlers. There are, however, constructors in
BeanTraderServiceClient's base type that match what you want. Since the autogenerated WCF client code all exists in partial classes, you can easily extend it. To do this, create a new file called BeanTraderServiceClient.cs and then create a partial class with that same name (using the BeanTrader.Service namespace). Then, add one constructor to the partial type as shown here.
public BeanTraderServiceClient(System.ServiceModel.InstanceContext callbackInstance) : base(callbackInstance, EndpointConfiguration.NetTcpBinding_BeanTraderService) { }
With those changes made, the Bean Trader sample will now be using a new .NET Standard-compatible WCF client and you can make the final fix of changing the
Open call in TradingService.cs to use
await OpenAsync instead.
With the WCF issues addressed, the .NET Core version of the Bean Trader sample now builds cleanly!
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. Once things build successfully, make sure the app runs and works as expected, especially if you are using any packages targeting .NET Framework.
Let's try launching the ported Bean Trader app and see what happens. The app doesn't get far before failing with the following exception.
System.Configuration.ConfigurationErrorsException: 'Configuration system failed to initialize' Inner Exception 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. If you wanted the WCF endpoint to be configurable in app.config, you could add it as an app setting and update the WCF client code to retrieve the WCF service endpoint from configuration.
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
BeginInvoke and
EndInvoke methods on delegate types due to underlying remoting dependencies. This issue and its fix are explained in more detail in the Migrating Delegate.BeginInvoke Calls for .NET Core blog post, but the gist is that
BeginInvoke and
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
Task.Run.
Task.Run(() => { return userInfoRetriever.Invoke(); }).ContinueWith(result => { // BeginInvoke's callback is replaced with ContinueWith var task = result.ConfigureAwait(false); CurrentTrader = task.GetAwaiter().GetResult(); }, TaskScheduler.Default);
After removing the
BeginInvoke usage, the Bean Trader app runs successfully on .NET Core!
All apps are different, so the specific steps needed to migrate your own apps to .NET Core will vary. But hopefully the Bean Trader sample demonstrates the general workflow and the types of issues that can be expected. And, despite this article's 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.