How to port desktop applications to .NET Core 3.0

Olia Gavrysh

In this post, I will describe how to port a desktop application from .NET Framework to .NET Core. I picked a WinForms application as an example. Steps for WPF application are similar and I’ll describe what needs to be done different for WPF as we go. I will also show how you can keep using the WinForms designer in Visual Studio even though it is under development and is not yet available for .NET Core projects.

About the sample

For this post, I’ll be using a Memory-style board game application. It contains a WinForms UI (MatchingGame.exe) and a class library with the game logic (MatchingGame.Logic.dll), both targeting .NET Framework 4.5. You can download the sample here. I’ll be porting the application project to .NET Core 3.0 and the class library to .NET Standard 2.0. Using .NET Standard instead of .NET Core allows me to reuse the game logic to provide the application for other platforms, such as iOS, Android or web.

You can either watch Scott Hunter and me doing the conversion in the following video, or you can follow the step-by-step instructions below. Of course, I won’t be holding it against you, if you were to do both.

Step-by-step process

I suggest doing the migration in a separate branch or, if you’re not using version control, creating a copy of your project so you have a clean state to go back to if necessary.

Before porting the application to .NET Core 3.0, I need to do some preparation first.

Preparing to port

  1. Install .NET Core 3 and Visual Studio 2019 Preview version (Visual Studio 2017 only supports up to .NET Core 2.2).
  2. Start from a working solution. Ensure the solution opens, builds, and runs without any issues.
  3. Update NuGet packages. It’s always a good practice to use the latest versions of NuGet packages before any migration. If your application is referencing any NuGet packages, update them to the latest version. Ensure your application builds successfully. In case of any NuGet errors, downgrade the version and find the latest one that doesn’t break your code.
  4. Run the .NET Portability Analyzer to determine if there are any APIs your application depends on that are missing from .NET Core. If there are, you need to refactor your code to avoid dependencies on APIs, not supported in .NET Core. Sometimes it’s possible to find an alternative API that provides the needed functionality.
  5. Replace packages.config with PackageReference. If your project uses NuGet packages, you need to add the same NuGet packages to the new .NET Core project. .NET Core projects support only PackageReference for adding NuGet packages. To move your NuGet references from packages.config to your project file, in the solution explorer right-click on packages.config -> Migrate packages.config to PackageReference….You can learn more about this migration in the Migrate from packages.config to PackageReference article.

Porting main project

Create new project

  1. Create new application of the same type (Console, WinForms, WPF, Class Library) as the application you are wanting to port, targeting .NET Core 3. At the moment we did the demo Visual Studio templates for desktop projects were under development, so I used the console.
    dotnet new winforms -o <path-to-your-solution>\MatchingGame.Core\
  2. In the project file copy all external references from the old project, for example:
    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
  3. Build. At this point if the packages you’re referencing support only .NET Framework, you will get a NuGet warning. If you have not upgraded to the latest versions of NuGet packages on the step 3, try to find if the latest version supporting .NET Core (.NET Standard) is available and upgrade. If there are no newer version, .NET Framework packages can still be used but you might get run-time errors if those packages have dependencies on APIs not supported in .NET Core. We recommend to let the author of the NuGet package know that you’d be interested in seeing the package being updated to .NET Standard. You can do it via Contact form on the NuGet gallery.

Fast way (replace existing project file)

First, let’s try the fast way to port. Make sure you have a copy of your current .csproj file, you might need to use it in future. Replace your current .csproj file with the .csproj file from the project you created on the step above and add in the top <PropertyGroup>:

<GenerateAssemblyInfo>false</GenerateAssemblyInfo>

Build your app. If you got no errors – congrats, you’ve successfully migrated your project to .NET Core 3. For porting a dependent (UI) project see Porting UI section, for using the Designer, check out Using WinForms Designer for .NET Core projects section.

Slow way (guided porting)

If you got errors (like I did with my app), it means there are more adjustments you need to make. Instead of the fast way described above, here I’ll do one change at a time and give possible fixes for each issue. Steps below would also help to better understand the process of the migration so if the fast way worked for you but you’re curious to learn all “whys”, keep on reading.

  1. Migrate to the SDK-style .csproj file. To move my application to .NET Core, first I need to change my project file to SDK-style format because the old format does not support .NET Core. Besides, the SDK-style format is much leaner and easier to work with.Make sure you have a copy of your current .csproj file. Replace the content of your .csproj file with the following. For WinForms application:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net472</TargetFramework>
        <UseWindowsForms>true</UseWindowsForms>
        <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
      </PropertyGroup>
    </Project>

    For WPF application:

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

    Note that I set <GenerateAssemblyInfo> to false. In the new-style projects AssemblyInfo.cs is generated automatically by default. So if you already have AssemblyInfo.cs file in your project (spoiler alert: you do), you need to disable auto-generation or remove the file.

    Now copy & paste all references from the old version of .csproj file into the new one. For example:

    NuGet package reference

    <PackageReference Include="Microsoft.Windows.Compatibility" Version="2.0.1" />

    Project reference

    <ProjectReference Include="..\MatchingGame.Core\MatchingGame.Core.csproj" />

    The project should build successfully since it is just a new way of writing the same thing. If you got any errors, double check your steps.

    There is also a third-party tool CsprojToVs2017 that can perform the conversion for you. But after using it, you still might need to delete some reference by hand, such as:

    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Net.Http" />
  2. Move from .NET Framework to .NET Standard or .NET Core. After successfully converting my library to SDK-style format I’m able to retarget it. In my case I want my class library to target .NET Standard instead of .NET Core. That way, it will be accessible from any .NET implementation if I decide to ship the game to other platforms (such as iOS, Android, or Web Assembly). To do so, replace this
    <TargetFramework>net472</TargetFramework>

    with

    <TargetFramework>netstandard2.0</TargetFramework>

    Build your application. You might get some errors if you are using APIs that are not included in .NET Standard. If you did not get any errors with your application, you can skip the next two steps.

  3. Add Windows Compatibility Pack if needed. Some APIs that are not included in .NET Standard are available in Windows Compatibility Pack. If you got errors on the previous step, you can check if Windows Compatibility Pack can help.I got an error “The name ‘Registry’ does not exist in the current context”, so I added the Microsoft.Windows.Compatibility NuGet package to my project. After installation, the error disappeared.
  4. Install API Analyzer. API Analyzer, available as the NuGet package Microsoft.DotNet.Analyzers.Compatibility, will prompt you with warnings when you are using deprecated APIs or APIs that are not supported across all platforms (Windows, Linux, macOS). If you added the Compatibility Pack, I recommend adding the API Analyzer to keep track of all of API usages that won’t work across all platforms.At this point, I am done with the class library migration to .NET Standard. If you have multiple projects referencing each other, migrate them “bottom-up” starting with the project that has no dependencies on other projects.In my example I also have a WinForms project MatchingGame.exe, so now I will perform similar steps to migrate that to .NET Core.

Porting the UI

  1. Add .NET Core UI project. Add a new .NET Core 3.0 UI project to the solution. At this moment, the Visual Studio templates for desktop projects are under development, so I just used the dotnet CLI.
    dotnet new winforms -o <path-to-your-solution>\MatchingGame.Core\

    For WPF projects you’d use this:

    dotnet new wpf -o <path-to-your-solution>\MatchingGame.Core\

    After my new WinForms .NET Core project was created, I added it to my solution.

  2. Link projects. First, delete all files from the new project (right now it contains the generic Hello World code). Then, link all files from your existing .NET Framework UI project to the .NET Core 3.0 UI project by adding following to the .csprojfile.
    <ItemGroup>
        <Compile Include="..\<Your .NET Framework Project Name>\**\*.cs" />
        <EmbeddedResource Include="..\<Your .NET Framework Project Name>\**\*.resx" />
    </ItemGroup>

    If you have a WPF application you also need to include .xaml files:

    <ItemGroup>
      <ApplicationDefinition Include="..\WpfApp1\App.xaml" Link="App.xaml">
        <Generator>MSBuild:Compile</Generator>
      </ApplicationDefinition>
      <Compile Include="..\WpfApp1\App.xaml.cs" Link="App.xaml.cs" />
    </ItemGroup>
    
    <ItemGroup>
      <Page Include="..\WpfApp1\MainWindow.xaml" Link="MainWindow.xaml">
        <Generator>MSBuild:Compile</Generator>
      </Page>
      <Compile Include="..\WpfApp1\MainWindow.xaml.cs" Link="MainWindow.xaml.cs" />
    </ItemGroup>
  3. Align default namespace and assembly name. Since you’re linking to designer generated files (for example, Resources.Designer.cs), you generally want to make sure that the .NET Core version of your application uses the same namespace and the same assembly name. Copy the following settings from your .NET Framework project:
    <PropertyGroup>
        <RootNamespace><!-- (Your default namespace) --></RootNamespace>
        <AssemblyName><!-- (Your assembly name) --></AssemblyName>
    </PropertyGroup>
  4. Disable AssemblyInfo.cs generation. As I mentioned earlier, in the new-style projects, AssemblyInfo.cs is generated automatically by default. At the same time the AssemblyInfo.cs file from the old WinForms project will be copied to the new project too, because I linked all files **\*.cs in the previous step. That will result in duplication of AssemblyInfo.cs. To avoid it in MatchingGame.Core project file I set GenerateAssemblyInfo to false.
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
  5. Run new project. Set your new .NET Core project as the StartUp project and run it. Make sure everything works.
  6. Copy or leave linked. Now instead of linking the files, you can actually copy them from the old .NET Framework UI project to the new .NET Core 3.0 UI project. After that, you can get rid of the old project.

Using the WinForms designer for .NET Core projects

As I mentioned above, the WinForms designer for .NET Core projects is not yet available in Visual Studio. However there are two ways to work around it:

  1. You can keep your files linked (by just not performing the previous step) and copy them when the designer support is available. This way, you can modify the files in your old .NET Framework WinForms project using the designer. And the changes will be automatically reflected in the new .NET Core WinForms project — since they’re linked.
  2. You can have two project files in the same directory as your WinForms project: the old .csproj file from the existing .NET Framework project and the new SDK-style .csproj file of the new .NET Core WinForms project. You’ll just have to unload and reload the project with corresponding project file depending on whether you want to use the designer or not.

Summary

In this blog post, I showed you how to port a desktop application containing multiple projects from .NET Framework to .NET Core. In typical cases, just retargeting your projects to .NET Core isn’t enough. I described potential issues you might encounter and ways of addressing them. Also, I demonstrated how you can still use the WinForms designer for your ported apps while it’s not yet available for .NET Core projects.

35 comments

Discussion is closed. Login to edit/delete existing comments.

  • Mike Ward 0

    Couple of porting questions:
    1. Is there a new browser control for WPF in .NET Core 3.0 or is the (very) old Win32 control the only option?
    2. Does .NET Core 3.0 resolve the “Air Space” issue when using Win32 controls?

  • yariker 0

    Will it be possible to use the new SDK-style .csproj files for WPF or WinForms projects targeting full .NET Framework?
    BTW, your “Migrate to the SDK-style .csproj file” first two code samples are swapped.

    • Stephen Donaghy 0

      Yes, you can actually do this today I believe. If you look at the part you’ve highlighted as being swapped around (which you are correct about), you can see that they’ve actually created an SDK style project targeting net472 – that’s your SDK project targeting the full framework.
       
      I’m not sure if the designer will still work or not, at least not without using the “linked” workaround but certainly the SDK project style works a treat. I just converted a load of projects to using it that are targeting .net 4.7.2 myself, in 2017.

  • Eddie de Bear 0

    Looks awesome. Now if you could only port WCF Server side over as well I might be able to migrate. 

  • Daren May 0

    A lot of non-trivial apps written for the enterprise “back in the day” used the patterns and practices Enterprise Library and Prism – running the app analyzer I see a number of things not supported in .NET Core – are then any plans to release “official” updated versions of the NuGET packages that are .NET Core .NET Standard 2.0 compliant (I do see another author has submited .NET Core versions but I can imagine some enterprises being reluctant to adopt those).
     

    T:System.Diagnostics.PerformanceCounterInstaller
    M:System.Diagnostics.PerformanceCounterInstaller.get_CategoryName
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Diagnostics.PerformanceCounterInstaller
    M:System.Diagnostics.PerformanceCounterInstaller.get_Counters
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Diagnostics.PerformanceCounterInstaller
    M:System.Diagnostics.PerformanceCounterInstaller.set_CategoryHelp(System.String)
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Diagnostics.PerformanceCounterInstaller
    M:System.Diagnostics.PerformanceCounterInstaller.set_CategoryName(System.String)
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Diagnostics.PerformanceCounterInstaller
    M:System.Diagnostics.PerformanceCounterInstaller.set_CategoryType(System.Diagnostics.PerformanceCounterCategoryType)
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.Installer
    T:System.Configuration.Install.Installer
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.Installer
    M:System.Configuration.Install.Installer.#ctor
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.Installer
    M:System.Configuration.Install.Installer.get_Context
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.Installer
    M:System.Configuration.Install.Installer.get_Installers
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.Installer
    M:System.Configuration.Install.Installer.Install(System.Collections.IDictionary)
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.Installer
    M:System.Configuration.Install.Installer.Uninstall(System.Collections.IDictionary)
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Security.Principal.WindowsImpersonationContext
    T:System.Security.Principal.WindowsImpersonationContext
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityIdentity
    T:System.EnterpriseServices.SecurityIdentity
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityIdentity
    M:System.EnterpriseServices.SecurityIdentity.get_AccountName
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityCallContext
    T:System.EnterpriseServices.SecurityCallContext
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityCallContext
    M:System.EnterpriseServices.SecurityCallContext.get_CurrentCall
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityCallContext
    M:System.EnterpriseServices.SecurityCallContext.get_DirectCaller
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityCallContext
    M:System.EnterpriseServices.SecurityCallContext.get_IsSecurityEnabled
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.EnterpriseServices.SecurityCallContext
    M:System.EnterpriseServices.SecurityCallContext.get_OriginalCaller
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessageQueueTransactionType
    T:System.Messaging.MessageQueueTransactionType
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessageQueue
    T:System.Messaging.MessageQueue
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessageQueue
    M:System.Messaging.MessageQueue.#ctor(System.String,System.Boolean,System.Boolean)
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessageQueue
    M:System.Messaging.MessageQueue.Close
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessageQueue
    M:System.Messaging.MessageQueue.get_Transactional
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessageQueue
    M:System.Messaging.MessageQueue.Send(System.Object,System.Messaging.MessageQueueTransactionType)
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Messaging.MessagePriority
    T:System.Messaging.MessagePriority
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.Configuration.Install.InstallerCollection
    T:System.Configuration.Install.InstallerCollection
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.Configuration.Install.InstallerCollection
    M:System.Configuration.Install.InstallerCollection.Add(System.Configuration.Install.Installer)
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

    T:System.AppDomain
    M:System.AppDomain.CreateDomain(System.String,System.Security.Policy.Evidence,System.AppDomainSetup)
    Microsoft.Practices.Prism.Composition
    Not supported

    T:System.AppDomain
    M:System.AppDomain.get_Evidence
    Microsoft.Practices.Prism.Composition
    Not supported

    T:System.Security.Principal.WindowsIdentity
    M:System.Security.Principal.WindowsIdentity.Impersonate(System.IntPtr)
    Microsoft.Practices.EnterpriseLibrary.Logging
    Not supported

    T:System.AppDomainSetup
    M:System.AppDomainSetup.get_ConfigurationFile
    Microsoft.Practices.EnterpriseLibrary.Common
    Not supported

  • bit bonk 0

    “If you have a WPF application you also need to include .xaml files“
    It seems that this is not necessary. Just like all the .cs files can be omitted from the project file this is also true for the .xaml files, which makes the project file even leaner.

  • Mark Adamson 0

    For single projects it’s worthwhile doing this manually as described, but if you have many projects to convert, it’s much easier with the mentioned convertor: https://github.com/hvanbakel/CsprojToVs2017 which can be used as a console application or customised in your own conversion tool by adding as a nuget package.

  • Jérôme Dubois 0

    OK, very good.
    But what for vb.net developpers? It seems there is no templates for VB.net and Winforms/WPF core.3.
    In the release version of VS 2019?
    DJ

    • Olia GavryshMicrosoft employee 0

      We ar working on bringing VB to .NET Core. templates are available for WinForms and WPF in CLI, we are working on adding them to Visual Studio as well. For now you can create a new WinForms project by running in CLI: dotnet new winforms –o <path>

  • Justin Lawrence 0

    Great video! Is the MyBlazorServerSideApp code available anywhere to take a look at? That was a great demo 

  • Tadeas Lejsek 0

    What would be the suggested approach on porting libraries with WPF user controls?

    • Olia GavryshMicrosoft employee 0

      General porting guidelines apply here, do you have any particular question or concern in mind?

  • George Danila 0

    This is all well and good, but what about real world scenarios? – LOB applications built on WPF and WinForms are usually very complex and make use of external libraries that will probably never get updated to run on .NET Core – I really can’t justify spending a huge amount of time and money to port our existing WPF solution to .NET Core.  My suggestion is for Microsoft to focus their efforts on bringing a new, cross-platform desktop framework for people to use on their greenfield projects and provide long-term support for WPF and WinForms on the .NET Framework.

Feedback usabilla icon