At Build 2019, Microsoft announced the release date for .NET Core 3.0 to be this coming September. This release includes the highly touted support for desktop platforms like WinForms and WPF. Today, there's still a large developer base that's building desktop applications using these .NET Windows desktop frameworks and by using .NET Core 3.0, you can now build desktop applications on the .NET Core platform.

Microsoft accomplishes this task via a new Windows Desktop package (similar to the ASP.NET platform package), a desktop project type that knows about WinForms and WPF, and runtime NuGet packages that allow targeting desktop-specific applications that can produce .NET executables on .NET Core. The Windows support libraries provide access to all of those Windows-specific APIs that the .NET Core Runtime doesn't support.

In this article, I look at what .NET Core support offers and what it means for existing desktop applications, followed by a practical example of porting a production WPF desktop application to .NET Core to demonstrate what's involved in that process.

Desktop Apps on .NET Core

First, let's discuss what .NET Core support means for Windows desktop applications. .NET Core conjures up all sorts of images: cross-platform support, better performance, many new framework enhancements over the full .NET Framework.

This is all true for .NET Core server applications, but for desktop applications, the story is a bit different. Running a Windows desktop application on .NET Core is much more mundane: The primary goal appears to be compatibility to run applications the same way as on the desktop framework but leveraging the .NET Core framework.

Microsoft is making a few promises for using .NET Core for your desktop apps:

  • Potentially better performance due to Core Runtime improvements
  • Control over what runtime to use
  • Support for new .NET Core and language features
  • Open source improvements to WPF and WinForms for .NET Core

I'll dig into these below, but let's start by clearing up a few misconceptions right away.

Moving to .NET Core won't make your Windows desktop application run on a Mac or Linux.

No, You Can't Run Your Windows Desktop App on a Mac or Linux!

Let's start with the most important point, because I've seen quite a few people reach the wrong conclusions due to the .NET Core Cross-Platform association:

Moving to .NET Core will not make your Windows Desktop apps run on a Mac or Linux.

Although the .NET Core Runtime is cross-platform and can run on Mac and Linux, when using desktop frameworks with .NET Core, your app still relies on the underlying Windows platform. WinForms uses the WindowsAPI and Hwnd style-rendering and WPF uses DirectX; both of those platforms are directly tied to the native Windows OS. Because the .NET Core Runtime doesn't support any platform-specific features on Windows, Mac, or Linux, the desktop runtime package provides Windows-specific NuGet packages that are referenced by desktop projects on Windows only. These packages are automatically referenced by a desktop project and provide access to all those Windows-specific APIs, like the WinForms and WPF frameworks, COM, Registry, Enterprise Services, System.Runtime access, and so forth.

In other words, all the desktop features that aren't part of the .NET Core Runtime are instead added as platform-specific NuGet packages and assemblies in your desktop project. Not everything is available, but most common features?and especially all the core Windows-specific CLR/BCL/FCL features?are available.

No Major Feature Changes

Once you convert a desktop application to run under .NET Framework, you're not going to see much in the way of new features or other changes. Above all, the goal of these support packages in this first release is compatibility so that you can run all your existing code on top of .NET Core. For the most part, that's the case. Although there are a few edge cases that might require workarounds, it's possible to run a full .NET Framework application on .NET Core with next to no changes, including being able to reference non-Core and non-.NET Standard components.

At the same time, it's important to understand that the desktop frameworks themselves haven't changed and there are very few new features. I'm OK with that! The initial release is not about features but about compatibility to run on .NET Core.

No Big Performance Improvements – Yet

.NET Core also isn't going to buy you much in terms of performance, at least not in its current state. As far as I can tell, so far, there are no amazing, blow-your-socks-off performance gains. In the application that I ported and discuss in this article, some brief profiling showed that the .NET Core version was slightly slower to start up (in Preview 5) and in overall application performance, the application didn't feel noticeably faster or slower for any operations. Performance for the few areas I profiled, other than startup, look and feel close to the same as in the full Framework. That's to be expected, because although .NET Core has some cool new tech in it for improved performance, most Windows-specific code doesn't take advantage of these new features.

Performance Potential

Microsoft mentions performance as a big reason to go to .NET Framework and although that may not yet be the case, there are several options on the horizon that make this more interesting in the future.

Tiered Compilation is a new feature in .NET Core that uses quick JIT compilation on startup of an application for a quicker and lighter startup footprint, with more optimized compilation performed in the background once the application is warmed up. This can help with improving startup speed. I turned on tiered compilation via project flag and saw slight improvements in startup time?nothing earth-shattering, but shaving off a half second of 2.5 second load time isn't bad. Even so, it didn't match the startup performance of the original full Framework application.

It's also likely that this new technology will be further tuned to produce even better startup compilation times geared specifically for desktop applications. Microsoft has also been talking about eventually providing Ahead of Time Compilation (AOT) that can produce native binaries that create self-contained applications with only those bits of code that your application actually touches. Some of this technology is already in use for UWP applications and for Xamarin on iOS, but it's still something on the roadmap for .NET Core in general. This has the potential to change how applications are delivered and executed.

In addition, .NET Core has many performance improvements that have greatly benefited the Web stack, but it requires specific optimization to use these improvements. Things like Span<T>, Memory<T>, and ArraySegment<T> are low-level system enhancements that can yield impressive memory and performance improvements. These new performance features are slowly finding their way into the Core CLR, where they bring improvements to all applications. Specific frameworks can also benefit as ASP.NET Core has, but it requires some investment into those frameworks to take advantage of these features. Whether Microsoft or the open source community maintaining WinForms and WPF will do any of that is anyone's guess.

Support for New CLR and Language Features

Microsoft recently announced that .NET 4.8 is the last full Framework .NET release with no more feature changes going forward. With full .NET Framework sent out to pasture for new feature development, all new features in the .NET stack are coming only to .NET Core going forward. At the Build conference, Microsoft also announced .NET 5.0, which is meant to be a convergence of the various .NETs that Microsoft now owns: .NET Core, full Framework, Xamarin, and Mono, in particular. In this ambitious announcement, Microsoft is trying to bring the best of each of these frameworks under a single hat and that hat is .NET Core, or, at that point, simply .NET.

If you want to continue to use new features in the CLR and C#, you no longer have a choice but to jump on the .NET Core bandwagon, because the full Framework as of .NET 4.8 won't see any new features. Already C# 8 is diverging enough that some of the requirements for this new language version aren't supported on full Framework. .NET Standard 2.1 also does not support full .NET Framework. If you want to use these features, .NET Core is your only choice.

Control Over the Runtime

One big complaint with full Framework desktop applications has been that applications are tied to the .NET Framework, which in turn is tied directly to Windows. Since Version 4.0, the .NET Framework has been using an unusual in-place update mechanism wherein each later version replaces the previous one. This means that installing a newer version can potentially break an older application that expects non-updated behavior. Although Microsoft has done an admirable job of maintaining backward compatibility, there have been a few hiccups over the years that have caused some people to cling to older versions of the .NET Runtime for specific behavior.

.NET Core addresses this problem by providing the options of side-by-side shared frameworks, or a fully self-contained installation that contains all the runtime files with your application. This makes it possible to ensure that you have exactly the right version of the framework that's required to run your application. If a compatible shared runtime or a dedicated install is available, the application can use that exact configuration.

Use the New Project Style for Desktop Apps

Along with the desktop-specific runtimes, there's also a new SDK-style desktop project that you can use:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

This desktop project style is much simpler, as it understands features of both WinForms and WPF. The project automatically handles all files that it knows about, automatically compiles XAML documents in WPF, picks up resource dependencies in WebForms, and so on. The result is that you end up with a project file that's very simple to understand and reason about. Rather than adding every single file to the project you want to compile, you invert that concept and only explicitly add files that are excluded or have special requirements, like Copy Local Content Files.

To be fair, this feature isn't tied to .NET Core 3.0 – you can also use this new project type with full Framework projects.

.NET 4.8 is the final full Framework version of .NET, according to Microsoft.

It's an Investment in the Future of .NET

Above all, moving to .NET Core with existing desktop applications currently isn't about new features or improved performance. Rather, it's about making an investment in moving to an evolving and continuing platform.

At Build 2019, Microsoft also announced that there won't be any new feature development for the full .NET Framework. .NET 4.8 is the last release of .NET Framework and it will be updated only for security patches and bug fixes. This means that full Framework libraries like ASP.NET Web Forms, Web Pages, WCF, and WorkFlow will be left as is. WPF, WinForms on the full Framework also won't see any new improvements. WPF and WinForms, however, live on in .NET Core and with those two frameworks now open source, we can hope that at least some minor improvements will show in the future.

Going forward, .NET Core is where any and all new feature improvements will be made. If you want to continue to see any of those improvements in your desktop development, .NET Core is the only way forward. WinForms and WPF also have been open sourced and are available now to outside contributions. There's a good chance that this will drive bug fixes and small new features that have been missing for many years on those platforms. But again, those changes only show only up on .NET Core in the platform specific NuGet packages and not on the full Framework, which will see no more updates going forward.

As you can see, above all, converting desktop applications to .NET Core is an investment in the continued future of .NET for existing desktop applications. It's not a requirement that you do it, but if you want to use updated features and take advantage of the newer technologies, .NET Core is the only option from here on forward.

Convert desktop applications to .NET Core for an investment in the future of .NET, not for features or performance improvements that aren't there yet.

Porting an Existing Application

Let's look at some nuts and bolts of moving a WPF application to .NET Core. I'll use one of my desktop applications (Markdown Monster) as an example. Markdown Monster is a full-featured Markdown editor and weblog publisher. It's extensible via .NET add-ins – something that's important in this upgrade context as I'll describe. It's what I'd call a medium-sized application that's mostly self-contained with the previously mentioned add-in model to provide additional extensibility and features. It also has a few edge-case scenarios – specifically COM interop with a JavaScript editor in the Web Browser control – which, as it turns out, was the biggest migration issue I had to deal with. I'll talk about that as I go through this process.

The Portability Analyzer

The first thing you should do before thinking about porting your application is to use the .NET Portability Analyzer) on your project to determine how compatible your application is. It's a Visual Studio extension that you run against your project and where you specify output targets that you want to check for. Once installed, you can use Analyze - Analyze Assembly Portability to point at your main assembly and let it loose.

The Analyzer is very good at finding things that aren't supported by a target platform both in your target assembly as well as its dependencies, and it produces a report that gives good pointers on areas that you need to fix. You can specify which versions of .NET to target and it spits out a report with potential issues. When I initially ran Markdown Monster against it, the analyzer found MM to be over 98% compatible. All the areas where there were problems were in library dependencies that the code path never touched. Figure 1 shows what the output from the Analyzer looks like.

Figure 1: Running the Portability Analyzer to find porting issues before you start
Figure 1: Running the Portability Analyzer to find porting issues before you start

Creating a New Project

The next step is to create a new SDK-style project for your existing application. This will likely be the most tedious step in the migration process as you have to create a new project from scratch, and then add all of your dependencies, content file settings, and special project behaviors back into this empty project.

Let's do it step by step. The first step is to create a new project and the first thing I did is create a new minimal project file:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
        <Version>1.16.2</Version>
        <AssemblyName>MarkdownMonster</AssemblyName>
        <UseWPF>true</UseWPF>
    </PropertyGroup>
</Project>

Notice the various special project type entries: The SDK, OutputType and UseWPF values are all specific to, in this case, a WPF project. For WinForms, change <UseWPF> to <UseWinForms>.

At this point, I can open the project in Visual Studio as a new SDK-style project.

Note that in the Markdown Monster example, I have one main EXE project and several support add-in projects that depend on the master project. This is backward from most projects that have a main project with support projects loaded by the main project. In the normal case you probably want to start upgrading the support projects first. If your support projects don't require desktop functionality, target them to .NET Standard 2.0 or 2.2 or .NET Core App and use a standard class library project, which won't need all the Windows desktop dependencies.

For example, for a .NET Standard project use:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <Version>3.0.26</Version>
    </PropertyGroup>
</Project> 

The bulk of the time spent porting to .NET Core involves creating the new SDK-style project.

Fix Assembly Attributes

To upgrade the main project, I first need to delete the existing Properties.cs file that holds assembly directives or add a <GenerateAssemblyInfo>false</GenerateAssemblyInfo> into the main <PropertyGroup>. Assembly attributes are now maintained in the project file itself using standard NuGet schema values and if you don't delete or use the attribute, they're duplicated, which causes a compilation error.

Add NuGet Packages

The next step is to add all the NuGet packages that were in packages.config into the project itself:

<ItemGroup>
    <PackageReference Include="MahApps.Metro" Version="1.6.5" />
    <PackageReference Include="Dragablz" Version="0.0.3.203" />
    <PackageReference Include="Westwind.Utilities" Version="3.0.25" />
    ...
</ItemGroup>

You only have to specify top-level NuGet packages. There's no need to add dependencies because they'll be pulled automatically, which cuts down on the number of packages compared to what's in package.config.

Note that not all the packages listed are .NET Core- or Standard-compliant, yet they still work. For example, I'm using the current version of MahApps and Dragablz, which are targeted for .NET 4.6, yet they work just fine. The compiler throws a warning on those that can't be targeted for .NET Core 3.0, but they still compile and work at runtime.

The Portability Analyzer and the compiler will let you know if there's a compile-time problem with using a specific library, but if there are problems in one of these full Framework assemblies using an API that's not supported, you'll likely find them at runtime. So far with Markdown Monster, all my 4.x assemblies work without a problem in .NET Core 3.0.

I had to fix a few small errors where there were library references in my own libraries that aren't available for .NET Core, but those were problems of my own making that you're unlikely to run into. Still, there might be a handful of odds and ends you have to fix before your project will build.

More Cowbell for Windows

If your project doesn't build and you get errors that complain about .NET Framework runtime libraries that appear to be missing, you might have to add an additional NuGet package:

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

The base Windows runtimes included in the desktop runtimes provide most of the desktop framework related Windows APIs plus all of their dependencies, which include a lot of the full Framework Windows functionality.

If you have some specific APIs that aren't found, you can add this package, which adds many more system-level Windows APIs. That means things like ODBC support, special COM interface operations, Enterprise Services, System.Runtime, WCF client, etc.

Adding References Explicitly

Still have problems with some APIs even after adding the references? You can also explicitly add libraries from the .NET Install folder and reference assemblies from there directly. You'll have to Copy Local to force the assemblies to be copied to your output folder in 3.0. This may or may not work based on whether all the required dependencies are available. You'll have to experiment to see what works and what doesn't.

I ran into one API – the WPF Speech API – that I couldn't get to work. I was able to get it to compile by copying it into my local output folder, but then the Speech API calls still failed with an internal error, presumably because some dependency is missing. Hopefully these troublesome APIs are far and few between, but if it doesn't work, you're out of luck. However, most common Windows APIs are supported and the compatibility footprint is very good for compatibility.

Adding Your Entry Point

The last thing I had to do to get the project to compile was to change the application entry point. Markdown Monster doesn't start with the default App.xaml but uses a custom Startup class that explicitly intercepts the application event loop to provide additional error handling.

If you're using an entry point other than app.xaml as I am, you'll have to add that explicitly. You can use Visual Studio's project dialog to pick the Startup class or add it to the main project group like this:

<StartupObject>StartUp</StartupObject>

At this point, I was able to get my project to compile.

Run It!

There are warnings for the full Framework assemblies in the project, but the project now compiles, as you can see in Figure 2.

Figure 2: The first successful project compilation. There are warnings for full Framework packages but they compile and run!
Figure 2: The first successful project compilation. There are warnings for full Framework packages but they compile and run!

These warnings just let you know that you have references in your project that aren't .NET Core- or .NET Standard-compliant, so there's a possibility that these assemblies will fail at runtime. Because they aren't .NET Standard or .NET Core assemblies, they might be referencing .NET Framework functions that aren't available, and when that happens, you have a runtime error on your hands. I haven't run into any of those in my project but be aware that there's a possibility for runtime failure.

Now I'm firing up my project for the first time, and I run into a runtime error immediately! A runtime error that complains about image resources that are missing, as shown in Figure 3.

Figure 3: Make sure that you add your WPF resources, like images, to the project explicitly.
Figure 3: Make sure that you add your WPF resources, like images, to the project explicitly.

Recall that an SDK-style projects knows about common file types like XAML files, .cs files to compile etc., but it can't automatically assume that all images are embedded resources, so they have to be added explicitly.

To do this, I need to add the compiled resources explicitly to the project:

<ItemGroup>
    <Resource Include="Assets/MarkdownMonster.png" />  
    <Resource Include="Assets/vsizegrip.png" />  
    <Resource Include="Assets/folder.png" />  
    <Resource Include="Assets/git.png" />
<ItemGroup>

I recompile and rerun now, but I get a different runtime error around missing content files on disk. Markdown Monster ships a bunch of HTML resources for the editor and preview, and those files need to be explicitly added into the project as well. There are also a couple of EXE support files for image compression and PDF generation, a Sample document, and a native DLL for Hunspell that has to live in the application's startup folder. All of these are content files and have to be added explicitly, with a specific option to Copy if Newer. I can add these to the project, as shown in Listing 1.

Listing 1: Don't forget to add content Files to your project

<ItemGroup>
    <None Update="Editor\**\*.*">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>  
    </None>  
    <None Update="PreviewThemes\**\*.*">    
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>  
    </None>  
    <None Update="Hunspellx86.dll">    
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>  
    </None>  
    <None Update="pingo.exe">    
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>  
    </None>  
    <None Update="wkhtmltopdf.exe">    
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>  
    </None>  
    <None Update="SampleMarkdown.md">    
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>  
    </None>
</ItemGroup>

Note that you can use wildcards for files and folders to make it easier to pull entire folder hierarchies with one single directive, which is very welcome compared to the old project style.

This completes all of the project file changes. If you want to look at the complete project file in one piece, head over to GitHub at: http://bit.ly/2JH5Xk5.

It's Alive!

Running again after adding resources and content files, the application now comes up. Yay! But all is not well: You can see in Figure 5 that something important is missing. Although the app is started now and running .NET Core, the main component – the Markdown editor – isn't loading.

Figure 4: The app is running but there's still a problem.
Figure 4: The app is running but there's still a problem.

Edge Cases: COM Interop and Dynamic

The document is loading and I can see it in the preview pane, but the Markdown Editor, which is a Web Browser control running a JavaScript Editor, is mysteriously missing.

The problem is probably an edge case and it relates to how Markdown Monster interfaces with the Web Browser control and the JavaScript editor component inside of it via COM. COM itself works just fine but accessing a COM object using the C# dynamic keyword turns out not to be supported in .NET Core yet. MM loads the HTML document that drives the editor and then retrieves a reference to the browser Window and the editor component inside of it. It then uses dynamic object access to drive the COM object. That no longer works in .NET Core 3.0 as it did in full Framework. You can see the runtime error in Figure 5.

Figure 5: COM Interop using dynamic doesn't work in .NET Core 3.0
Figure 5: COM Interop using dynamic doesn't work in .NET Core 3.0

The COM reference is retrieved fine, but dynamic access to the parentWindow property causes the application to fail. Using Reflection to retrieve the same property works. Microsoft is aware of this issue and has marked this feature to be added, but unfortunately it won't be fixed for .NET Core 3.0, although it will be added later. Just to be clear though: COM access via Reflection works, and dynamic access to .NET objects works, but using dynamic with COM objects doesn't work.

The workaround for this scenario is to use Reflection. To get the application to come up initially, I changed the dynamic startup code to use Reflection helpers like this:

object window = ReflectionUtils.GetPropertyCom(doc, "parentWindow");
AceEditor = ReflectionUtils.InvokeMethod(window, "InitializeInterop", this);
if (AceEditor != null)
{
    // Doesn't work in Core 3.0
    //AceEditor.setvalue(markdown ?? string.Empty, position, keepUndoBuffer);
    ReflectionUtils.CallMethodCom(AceEditor, "setvalue", markdown ?? string.Empty, position, keepUndoBuffer);
}

With a couple of manual Reflection fixes I can get the app running. Figure 6 shows the running application that properly handles the Editor's COM interop. Unfortunately, this means that the app is running cosmetically only. There are many COM Interop operations using dynamic so I have a lot of work ahead of me fixing all of those instances properly.

Figure 6: At last: The application is running under .NET Core.
Figure 6: At last: The application is running under .NET Core.

Although the application loaded, now there are many more COM interop calls that control the Editor behavior. I can type into the Editor but most menu operations on the Editor just fail silently because the Editor instance is null and those commands have no effect. Ouch. For MM that's a pretty big problem and that was quite discouraging – enough so that I called it a day.

In the following days, I ended up refactoring all COM interaction into separate feature-specific classes for the Editor and Previewer that isolate the COM calls in one place and the Editor and Previewer both work on .NET Core. You can check out the refactored AceEditor Interop component here: http://bit.ly/2LFewhF.

This is a breaking change and although not every application uses COM and dynamic together, there's quite a bit of desktop code that does. A lot of the Office Automation libraries depend on dynamic COM interop, as do many other script integrations. My SnagIt Screen capture interface in Markdown Monster also broke with the same dynamic issues and had to be fixed also.

This is one of the caveats of migrating to .NET Core – you may end up finding some obscure feature that you forgot you were even using, only to find that it's not supported in .NET Core. And in this case, too, the only way you find this problem is at runtime. In MM's case, the problem is pretty obvious because dynamic COM Interop is used all over the place so the problem was easy to notice. But an isolated instance like, say, the screen capture functionality, would be a lot less likely to be found immediately. So, it's important to thoroughly test all functionality of an application to make sure everything works as expected.

In my case, I'm also lucky that there's a workaround by falling back to Reflection. For other cases there may be no workaround. Even with my Reflection workaround, I ended up spending a few hours refactoring code to make it all work, which was more time than I spent on the rest of the porting process. The good news is, I ended up with better more portable code, which might be useful for a future move to the WebView control using Edge Chromium once that becomes available.

At this point, 100% of Markdown Monster is running under .NET Core. That's pretty cool.

Adding Add-ins

With the main application running, my next step was to add the add-in projects. This was both easier and more complicated at the same time. Easier, because it was easy to get the project to compile and work under .NET Core 3.0. The steps are nearly identical to the main project because these projects also have UI components that use the same Desktop project type and settings.

Because the add-ins are much smaller and have fewer dependencies, it was quick to get them converted. It literally took a few minutes to get the first add-in to compile and run. I was able to copy the existing project structure from the main project, rename for my assembly, add a project reference to the main Markdown Monster project, change the package references, and things just worked on the first attempt, with add-ins loading right up. Nice!

But there were still complications, mainly because of the way that add-ins work in Markdown Monster. Add-ins are loaded from a special folder called Addins in the main project's output folder and output should be generated there. Add-ins also have dependencies that already exist in the main application and I don't want those dependencies copied into the output folder. Basically, I want the “Copy Local: False” behavior. It turns out that there are a few changes and problems how dependencies are generated into the output folder. In fact, I was unable to set a project reference and have it not generate output files into the target folder (i.e., Copy Local: False for a project reference) and instead had to fall back to using an explicit assembly reference in the project.

You can check out the full WebLogAddin project file at http://bit.ly/2E5Cn3V. Specifically, look for the OutDir tab, and the IncludeAssets references for NuGet packages, which keeps the NuGet packages from copying local.

Now What?

At this point, I have a running .NET Core 3.0 application that has all of the features of the classic .NET application: nothing more, nothing less. The application looks the same, it doesn't run any faster, and even the code isn't really very different. Other than using a new project type and running on a new runtime, nothing has really changed.

Well almost. There have already been a few very minor changes in the code of desktop platforms that are useful. For example, the Windows Folder Forms Browser dialog in full Framework is a painful dialog to navigate as you can't paste a path, and it uses some arcane controls to navigate the browser. One of the first changes submitted in the open source WinForms implementation was to provide a new folder dialog using the same old syntax but the much richer and more modern Explorer-style dialog shown in Figure 7.

Figure 7: One of the first improvements in Desktop platforms is the Explorer-based Folder Browser.
Figure 7: One of the first improvements in Desktop platforms is the Explorer-based Folder Browser.

This is a trivial change, but this community-submitted fix to WinForms made it into the new release and is now available in .NET Core 3.0's desktop support. Best of all, no code changes are required. I think we'll see a lot more of these small but very useful changes in the base frameworks in the future based on community updates – that's one of the big benefits of open sourcing libraries even as complex as WinForms or WPF.

.NET Core applications can run either using a pre-installed shared runtime or by creating a fully self-contained install.

Distribution

Now that I have a running application, the next big question is: How do I distribute it?

I have two options:

  • Use a shared preinstalled desktop runtime
  • Build a self-contained installation

Shared Runtimes

Using the shared runtime means that I need to install two separate runtimes: The .NET Core Runtime, plus the Desktop Runtime Pack. This is similar to the way ASP.NET Core installs, which also requires the .NET Core Runtime, plus the ASP.NET Core Runtime Package.

It may seem tedious to require these separate packages, but Microsoft wants to keep the base .NET Core package as small as possible with other frameworks provided as separate add-on packages to provide framework functionality. Microsoft then ships feature packs that combine the Runtime plus Framework. Today, Microsoft does this with the Windows Hosting Pack, which packages .NET Core, ASP.NET Core, and the Windows Hosting Module into a single package that's ready to go. Something similar will be available for desktop apps to combine the base runtime and desktop frameworks.

As of Preview 5, Microsoft has no pre-packaged Desktop Runtime Package ready yet, so the only way to install the shared Desktop Runtime currently is by installing the .NET Core 3.0 SDK. According to Microsoft, a desktop-specific package will be available soon, so by the time you read this article, it might be available already at http://dot.net. For now, the SDK includes the .NET Core Runtime, the ASP.NET Core, and Desktop Runtimes plus the SDK features. If you're building a 32-bit application, as I did for Markdown Monster, make sure you also install the 32-bit SDK or runtime once it becomes available.

One additional important point about shared runtimes is that .NET Core applications target a specific .NET Core runtime version and a compatible version needs to be installed on the target computer at runtime. Compatible means either the same version or a higher minor version must be available for your application to run. If you build your application for .NET Core 3.0.1 and only .NET Core 3.1.1 is installed, your application still works, but if only 3.0 is installed, it won't launch and you get a runtime launch error, as shown in Figure 8.

Figure 8: Runtime Launch Error
Figure 8: Runtime Launch Error

This makes runtime selection trickier than it was with full Framework as you can make no good guesses as to what's installed on a client's computer. Microsoft is promising better installer tools to make it easier to build installations that conditionally download and install runtimes as needed, but that technology hasn't arrived yet.

Self-Contained Installations

A self-contained installation does exactly what it sounds like: It creates an installation that includes all the required .NET Core Runtime files in your install folder so that your application doesn't have any runtime requirements on the target computer. To do this, you can add the following to your project file:

<SelfContained>true</SelfContained>

As you might expect, this isn't a small installation, as shown in Figure 9.

Figure 9: A self-contained runtime installation isn't small at 190mb.
Figure 9: A self-contained runtime installation isn't small at 190mb.

The Markdown Monster installation clocks in at nearly 200mb in self-contained mode. A compressed version in a 7z archive clocks in at around 55mb. For reference, using a shared install creates around a 90mb install and ~15mb zipped.

The nice thing about a self-contained install is that it includes all dependencies in the project. You can move that application to any computer, including one that doesn't have any version of .NET Core installed, and run it there.

This is obviously useful, especially in corporate scenarios where many applications have to co-exist and where you can ensure that your application gets the exact runtime that you built it with during development.

Personally, I plan on using shared distribution – eventually. Self-contained distribution is simply too large currently as it would quadruple my current installer size.

Today, it makes little to no sense to ship a .NET Core 3.0 application. Even when it eventually ships at the end of the year, I don't think I'll be ready to jump onto .NET Core 3.0 distribution right away. Between the runtime requirements and the fact that there's no tangible feature or performance benefit to my customers, I see no reason to ship a .NET Core 3.0 version any time soon.

Even so, I don't regret putting in the time to port to .NET Core 3.0 because I think that in the future, there may very well be good reasons to run on .NET Core, if nothing else, just to stay up with the latest .NET Core runtime improvements.

Done and Done

And with that, I'm done. Markdown Monster runs on .NET Core 3.0 and although for now, I'll stick with the full .NET Framework, I can now keep this application in sync with .NET Core. When the time comes to go all in on .NET Core because of new must-have features or performance or a more compelling deployment story, I'll be ready to switch easily. Today I'm dual targeting Markdown Monster to both .NET Core 3.0 and .NET 4.6.2, which allows me to keep an eye out for breaking changes that I might introduce to the .NET Core code.

At the end of the day, I see .NET Core 3.0 in its current state for its potential for future improvements that are beneficial for desktop applications, much more so than for the current set of features, which don't offer anything new and exciting yet. Whether we'll see improvements is another story, but even without desktop framework changes, .NET Core Runtime and C#–and other language improvements–might be enough reason to make .NET Core 3.0 the future target of choice.

Only time will tell?