Migrating a Sample WPF App to .NET Core 3 (Part 1)
Olia recently wrote a post about how to port a WinForms app from .NET Framework to .NET Core. Today, I’d like to follow that up by walking through the steps to migrate a sample WPF app to .NET Core 3. Many of these steps will be familiar from Olia’s post, but I’ve tried to differentiate this one by including some additional common dependencies that users are likely to run into like WCF client usage or third-party UI packages.
To keep this post from being too long, I will split it into two parts. In this first part, we’ll prepare for migration and create a new csproj file for the .NET Core version of the app. In the second post, we’ll make the actual code changes necessary to get the app working on .NET Core.
These posts don’t focus on any one particular porting issue. Instead, they’re meant to give an overview of the steps needed to port a sample WPF app. If there are particular .NET Core migration topics you’d like a deeper look at, let us know in the comments.
If you would prefer a video demonstration of migrating the application, I have posted a series of YouTube videos of me porting the app.
About the sample
For this exercise, I wrote a simple commodity trading app called ‘Bean Trader’. Users of the app have accounts with different numbers of beans (which come in four different colors). Using the app, users can propose and accept trades with other users. The app isn’t particularly large (~2,000 lines of code), but is meant to be a step up from ‘Hello World’ in terms of complexity so that we can see some issues users may encounter while porting real applications.
Interesting dependencies in the app include:
- WCF communication with a backend trading service via a duplex NetTcp channel
- UI styling and dialogs from MahApps.Metro
- Dependency injection with Castle.Windsor (though, of course, many DI solutions – including Microsoft.Extensions.DependencyInjection – could be used in this scenario)
- App settings in app.config and the registry
- A variety of resources and resx files
The app source is available on GitHub in case you want to follow along with this blog post. The original source (prior to porting) is available in the NetFx\BeanTraderClient directory. The final, ported application is in the NetCore\BeanTraderClient directory. The backend service, which the app needs to communicate with, is available in the BeanTraderServer folder.
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, I’ve deliberately included some anti-patterns in the app to make sure we encounter at least a couple interesting challenges while porting.
Migration process overview
- First, it is useful to prepare for the migration by understanding the project’s dependencies and getting the project into an easily portable state.
- This includes using tools like the .NET Portability Analyzer to understand .NET Framework dependencies.
- It also includes updating NuGet references to use the
<PackageReference>format and, possibly, updating NuGet package versions.
- Second, the project file needs to be updated. This can be done either by creating a new project file or by modifying the current one in-place.
- Third, the source code may need some updates based on different API surface areas either in .NET Core or in the .NET Core versions of required NuGet packages. This is typically the step that takes the longest.
- Fourth, don’t forget to test the migrated app! Some .NET Core/.NET Framework differences don’t show up until runtime (though there are Roslyn code analyzers to help identify those cases).
Step 1: Getting ready
Have the sample cloned and ready to go? Great; let’s dive in!
The primary challenge with migrating a .NET Framework app to .NET Core is always that its dependencies may work differently (or not work at all!) on .NET Core. Migration is much easier than it used to be – many NuGet packages now target .NET Standard and, starting with .NET Core 2.0, the .NET Framework and .NET Core surface areas have become quite similar. Even so, some differences (both in support from NuGet packages and in available .NET APIs) remain.
<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 differently, though. It uses
<PackageReference> elements in the csproj file itself (rather than in a separate config file) to reference NuGet dependencies. Fortunately, old-style csproj files can also use this more modern syntax.
When migrating, there are two advantages to using
- This is the style of NuGet reference that will be required for the new .NET Core project file. If you’re already using
<PackageReference>, those project file elements can be copied and pasted directly into the new project.
- Unlike a packages.config file,
<PackageReference>elements only refer to the top-level dependencies that your project depends on directly. All other transitive NuGet packages will be determined at restore time and recorded in the autogenerated obj\project.assets.json file. This makes it much easier to reason about what dependencies your project has, which is useful when determining whether the necessary dependencies will work on .NET Core or not.
So, the first step to migrating the Bean Trader sample is to migrate it to use
<PackageReference> NuGet references. Visual Studio makes this simple. Just right-click on the project’s packages.config file in Visual Studio’s
solution explorer and select ‘Migrate packages.config to PackageReference’.
A dialog will appear 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 will be removed and
<PackageReference> elements will be added to the project file.
<PackageReference>-style references don’t store NuGet packages locally in a ‘packages’ folder (they are stored globally, instead, as an optimization) so, after the migration completes, you will need to edit the csproj file and remove the
<Analyzer> elements referring to the FxCop analyzers that previously came from the ..\packages directory. Don’t worry – since we still have the NuGet package reference, the analyzers will be included in the project. We just need to clean up the old packages.config-style
Review NuGet packages
Now that it’s easy to see the top-level NuGet packages our project depends on, we can review whether those packages will be available on .NET Core or not.
You can know whether a package supports .NET Core by looking at its dependencies on nuget.org. The community-created fuget.org site also shows this information prominently at the top of the package information page.
When targeting .NET Core 3, any packages targeting .NET Core or .NET Standard should work (since .NET Core implements the .NET Standard surface area). 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 is not present in .NET Core, you will 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.
In the case of the Bean Trader sample, we have the following top-level NuGet dependencies:
- Castle.Windsor, version 4.1.1. This package targets .NET Standard 1.6, so it will work on .NET Core.
- Microsoft.CodeAnalysis.FxCopAnalyzers, version 2.6.3. This is a meta-package so it’s not immediately obvious which platforms it supports, but documentation indicates that its newest version (2.9.2) will work for both .NET Framework and .NET Core.
- Nito.AsyncEx, version 4.0.1. This package does not target .NET Core, but the newer 5.0 version does. This is common when migrating because many NuGet packages have added .NET Standard support over the last year or so, but older projects will be using older versions of these packages. 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, we will 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, does not target .NET Core, but has a newer pre-release (2.0-alpha) that does. Again, we will 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.
If there had been packages that didn’t target .NET Core or .NET Standard, we would 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, so we could search for those. Enterprise Library packages are an example of the community publishing “.NetCore”-suffixed alternatives.
- If no alternatives are available, we can proceed using the .NET Framework-targeted packages, bearing in mind that we will need to test them thoroughly once running on .NET Core.
Upgrade NuGet packages
If possible, it would be good to upgrade the versions of these packages to ones that target .NET Core or .NET Standard at this point 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 we have a new project file targeting .NET Core. If possible, though, upgrading the NuGet packages to .NET Core-compatible versions ahead of time makes the migration process even easier once it comes time to create the new project file and reduces the number of differences between the .NET Framework and .NET Core versions of the app.
In the case of the Bean Trader sample, all of the necessary upgrades can be made easily (using Visual Studio’s NuGet package manager) with one exception: upgrading from MahApps.Metro 1.6.5 to 2.0 reveals breaking changes related to theme and accent management APIs.
Ideally, the app would be updated to use the newer version of the package (since that is more likely to work on .NET Core). In some cases, though, that may not be feasible. In this case, I’m not going to upgrade MahApps.Metro becase the necessary changes are non-trivial and this walkthrough is supposed to focus on migrating to .NET Core 3, not to MahApps.Metro 2. Also, this is a low-risk .NET Framework dependency because the Bean Trader app only exercises a small part of MahApps.Metro. It will, of course, require testing to make sure everything’s working once the migration is complete. If this were a real-world scenario, I would file an issue to track the work to move to MahApps.Metro version 2.0 since not doing the migration now leaves behind some technical debt.
Once the NuGet packages are updated to recent versions, the
<PackageReference> item group in the Bean Trader sample’s project file should look like this:
.NET Framework portability analysis
Now that we feel good about the state of the project’s NuGet dependencies, let’s consider .NET Framework API dependencies. The .NET Portability Analyzer tool is useful for understanding which of the .NET APIs your project uses are available on other .NET platforms.
In Olia’s previous post, she used the GUI, so I’ll use the command line interface for variety. The necessary steps are:
- Download the API 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 anyhow!).
- Run API Port with a command line like this:
ApiPort.exe analyze -f <PathToBeanTraderBinaries> -r html -r excel -t ".NET Core"
-fargument specifies the path containing the binaries to analyze. The
-rargument specifies which output file format you want. I find both HTML and Excel outputs useful. The
-targument specifies which .NET platform we are analyzing API usage against. In this case, we want .NET Core since that’s the platform we are moving to. Since no version is specified, API Port will default to the latest version of the platform (.NET Core 3.0 in this case).
When you open the HTML report, the first section will list all of the binaries that were analyzed and what percentage of the .NET APIs they use are available on the targeted platform. The percentage is not very meaningful by itself. What’s more useful is to see the specific APIs that are missing. To do that, either click an assembly name or scroll down to the reports for individual assemblies.
You only need to be concerned about assemblies that you own the source code for. In the Bean Trader ApiPort report, there are a lot of binaries listed, but most of them belong to NuGet packages. Castle.Windsor, for example, shows that it depends on some System.Web APIs that are missing in .NET Core. This isn’t a concern, though, because we previously verified that Castle.Windsor supports .NET Core. It is common for NuGet packages to have different binaries for use with different .NET platforms, so whether the .NET Framework version of Castle.Windsor uses System.Web APIs or not is irrelevant as long as the package also targets .NET Standard or .NET Core (which it does).
In the case of the Bean Trader sample, the only binary that we need to consider is BeanTraderClient and the report shows that only two .NET APIs are missing –
These are unlikely to be blocking issues because WCF Client APIs are (mostly) supported on .NET Core, so there must be alternatives available for these central APIs. In fact, looking at
System.ServiceModel‘s .NET Core surface area (using https://apisof.net), we 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. We’re ready for the next step in which we’ll actually start the migration.
Step 2: Migrating the project file
Because .NET Core uses the new SDK-style project file format, our existing csproj file won’t work. We’re going to need a new project file for the .NET Core version of the Bean Trader app. If we didn’t need to build the .NET Framework version of the app going forward, we could just replace the existing csproj file. But, often, developers want to be able to build both versions – especially for the time being since .NET Core 3 is still in preview.
There are three options for where the new csproj file should live, each of which has its own pros and cons:
- We can use multi-targeting (specifying multiple
<TargetFrameworks>targets) to have a single project file that builds both .NET Core and .NET Framework versions of the solution. In the future, this will probably be the best option. Currently, though, a number of design-time features don’t work well with multi-targeting. So, for now, the recommendation is to have separate project files for .NET Core and .NET Framework-targeted versions of the app.
- We can put the new project file in a different directory. This makes it easy to keep build output separate, but means that we won’t be taking advantage of the new project system’s ability to automatically include C# and XAML files. Also, it will be necessary to include
<Link>elements for XAML resources so that they are embedded with correct paths.
- We can put the new project file in the same directory as the current project file. This avoids the issues of the previous option but will cause the obj and bin folders for the two projects to conflict. If you only open one of the projects at a time, this shouldn’t be an issue. But if they will both be open simultaneously, you will need to update the projects to use different output and intermediate output paths.
I prefer option 3 (having the project files live side-by-side), so I will use that approach for this porting sample.
To actually create the new project file, I usually use a
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 migration. In my experience, the tool is helpful but still needs a human to review the results to make sure all the details of the migration are correct. One particular area that the tool doesn’t handle optimally is migrating NuGet packages from packages.config files. If the tool runs on a project file that still uses a packages.config file to reference NuGet packages, it will migrate to
<PackageReference> elements automatically, but will add
<PackageReference> elements for all of the packages instead of just for top-level ones. If you have already migrated to
<PackageReference> elements with Visual Studio, though (as we have done in this case), then the tool can help with the rest of the conversion. Like Scott Hanselman recommends in his blog post on migrating csproj files, I think porting by hand is educational and will give better results if you only have a few projects to port. But if you are porting dozens or hundreds of project files, then a tool like CsprojToVs2017 can be a big help.
So, if you’re following along at home, run
dotnet new wpf in a temporary directory and move the generated csproj file into the BeanTraderClient folder and rename it BeanTraderClient.Core.csproj.
Because the new project file format automatically includes C# files, resx files, and XAML files that it finds in or under its directory, the project file is already almost complete! To finish the migration, I like to open the old and new project files side-by-side and look through the old one seeing if any information it contains needs to be migrated. In this case, the following items should be copied to the new project:
<ApplicationIcon>properties should all be copied.
- I 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 auto-generate these attributes based on properties in the csproj file. Because we don’t want that to happen in this case (the auto-generated attributes would conflict with those from AssemblyInfo.cs), we disable the auto-generated attributes with
- 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. We can simplify the png references to a single line by using the new project file format’s support for globbing patterns:
<Resource Include="**\*.png" />.
<None>items will be included automatically, but they will not be 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), we need to update the automatically populated
<None>item for that file, like this:
- The Bean Trader sample includes a XAML file (Default.Accent.xaml) as
Content(rather than as a
Page) because themes and accents defined in this file are loaded from the file’s XAML at runtime, rather than being embedded in the app itself. The new project system automatically includes this file as a
<Page>, of course, since it’s a XAML file. So, we need to both
remove the XAML file as a page (
<Page Remove="**\Default.Accent.xaml" />) and add it as content:
- Finally, add NuGet references by copying the
<ItemGroup>with all the
<PackageReference>elements. If we hadn’t previously upgraded the NuGet packages to .NET Core-compatible versions, we 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 the solution explorer and
dotnet restore BeanTraderClient.Core.csproj should successfully restore packages (with two expected warnings related to the MahApps.Metro version we’re using targeting .NET Framework).
This is probably a good breaking point between parts one and two of this article. In the second post, we’ll get the app building and running against .NET Core.