Packaging a .NET Core app with the Desktop Bridge
Update: Starting with Visual Studio 2019 Preview 2, the steps outlined below aren't necessary as the functionality is built-in. Just create a Packaging Project and add a reference to your desktop application and it'll "do the right thing."
The Windows Desktop Bridge is a way to package up Desktop applications for submission to the Microsoft Store or sideloading from anywhere. It's one of the ways of creating an MSIX package, and there's a lot more information about the format in the docs. The short version is this: think about it like the modern ClickOnce. It's a package format that supports automatic updating while users the peace of mind that it won't put bits all over their system or pollute the registry.
Earlier today, Microsoft announced the first previews of .NET Core 3 and Visual Studio 2019. These previews have support for creating Desktop GUI apps with .NET Core using WPF and Windows Forms. It's possible to migrate your existing app from the .NET Framework to .NET Core 3. I'll blog about that in a later post, but it can be pretty straight-forward for many apps. One app that has already made the switch is NuGet Package Explorer; it's open-source on GitHub and may serve as a reference.
Once you have an application targeting .NET Core 3, some of your next questions may be, "how do I get this to my users?" ".NET Core 3 is brand new, my users won't have that!" "My IT department won't roll out .NET Core 3 for a year!"
Sound familiar? One of the really cool things (to me) in .NET Core is that it supports completely self-contained applications. That is to say it has no external dependencies. Nothing needs to be installed on the machine, not even .NET Core itself. You can
xcopy the publish output from the project and give it to someone to run. This unlocks a huge opportunity as you, the developer, can use the framework and runtime versions you want, without worrying about interfering with other apps on the machine, or even if the runtime exists on the box.
With the ability to have a completely self-contained app, we can take advantage of the Desktop Bridge to package our app for users to install. As of today, the templates don't support this scenario out-of-the-box, but with a few tweaks, we can make it work. Read on for the details.
You'll need Visual Studio 2017 15.9, or better yet, the Visual Studio 2019 preview, just released today. In the setup, make sure to select the UWP workload to install the packaging project tools. Grab the .NET Core 3 preview and create your first WPF .NET Core app with it.
The official docs show how to add a Packaging project to your solution, so we'll pick-up after that article ends. Start with that first. In the future, once the tooling catches up, that's all you'll need. For now, as a temporary workaround, the rest of this post describes how to make it work.
The goal here is get the packaging project to do a self-contained publish on the main app and then use those outputs as its inputs for packing. This requires changes to two files
- The main application project, NetCoreDesktopBridgeApp.csproj in the sample.
- The packaging project, NetCoreDesktopBridgeApp.Package.wapproj in the sample.
Let's start with the main application project, the
.vbproj file. Add
<RuntimeIdentifiers>win-x86</RuntimeIdentifiers> to the first
<PropertyGroup>. This ensures that NuGet restore pulls in the runtime-specific resources and puts them in the
project.assets.json file. Next, put in the following
<Target Name="__GetPublishItems" DependsOnTargets="ComputeFilesToPublish" Returns="@(_PublishItem)"> <ItemGroup> <_PublishItem Include="@(ResolvedFileToPublish->'%(FullPath)')" TargetPath="%(ResolvedFileToPublish.RelativePath)" OutputGroup="__GetPublishItems" /> <_PublishItem Include="$(ProjectDepsFilePath)" TargetPath="$(ProjectDepsFileName)" /> <_PublishItem Include="$(ProjectRuntimeConfigFilePath)" TargetPath="$(ProjectRuntimeConfigFileName)" /> </ItemGroup> </Target>
The full project file should look something like this:
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <UseWPF>true</UseWPF> <!-- Use RuntimeIdentifiers so that the restore calculates things correctly We'll pass RuntimeIdentifier=win-x86 in the reference from the Packaging Project --> <RuntimeIdentifiers>win-x86</RuntimeIdentifiers> </PropertyGroup> <!-- Add the results of the publish into the output for the package --> <Target Name="__GetPublishItems" DependsOnTargets="ComputeFilesToPublish" Returns="@(_PublishItem)"> <ItemGroup> <_PublishItem Include="@(ResolvedFileToPublish->'%(FullPath)')" TargetPath="%(ResolvedFileToPublish.RelativePath)" OutputGroup="__GetPublishItems" /> <_PublishItem Include="$(ProjectDepsFilePath)" TargetPath="$(ProjectDepsFileName)" /> <_PublishItem Include="$(ProjectRuntimeConfigFilePath)" TargetPath="$(ProjectRuntimeConfigFileName)" /> </ItemGroup> </Target> </Project>
Next up, we need to add a few things to the packaging project (
.wapproj). In the
<PropertyGroup> that has the
EntryPointProjectUniqueName, add another property:
<DebuggerType>CoreClr</DebuggerType>. This tells Visual Studio to use the .NET Core debugger. Note: after setting this property, you may have to unload/reload the project for VS to use this setting, if you get a weird debug error after changing this property, restart VS, load the solution and it should be fine.
Next, look for the
<ProjectReference ... element. If it's not there, right click the Application node and add the application reference to your main project. Add the following attributes:
SkipGetTargetFrameworkProperties="true" Properties="RuntimeIdentifier=win-x86;SelfContained=true". The full
ItemGroup should look something like this:
<ItemGroup> <!-- Added Properties to build the RID-specific version and be self-contained --> <ProjectReference Include="..\NetCoreDesktopBridgeApp\NetCoreDesktopBridgeApp.csproj" SkipGetTargetFrameworkProperties="true" Properties="RuntimeIdentifier=win-x86;SelfContained=true" /> </ItemGroup>
Finally, and we're almost done, add the following snippet after the
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" /> line:
<!-- Additions for .NET Core 3 target --> <PropertyGroup> <PackageOutputGroups>@(PackageOutputGroups);__GetPublishItems</PackageOutputGroups> </PropertyGroup> <Target Name="_ValidateAppReferenceItems" /> <Target Name="_FixEntryPoint" AfterTargets="_ConvertItems"> <PropertyGroup> <EntryPointExe>NetCoreDesktopBridgeApp\NetCoreDesktopBridgeApp.exe</EntryPointExe> </PropertyGroup> </Target> <Target Name="PublishReferences" BeforeTargets="ExpandProjectReferences"> <MSBuild Projects="@(ProjectReference->'%(FullPath)')" BuildInParallel="$(BuildInParallel)" Targets="Publish" /> </Target>
In that snippet, change
NetCoreDesktopBridgeApp\NetCoreDesktopBridgeApp.exe to match your main project's name and executable.
Bonus section: as a point-in-time issue, you'll need to declare a package dependency on the VCRedist in your Package.appxmanifest file. Add the following in the
<PackageDependency Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" Name="Microsoft.VCLibs.140.00.UWPDesktop" MinVersion="14.0.26905.0" />. When your users install the app, Windows will automatically pull that dependency from the store.
Build & Debug
With the above pieces in place, you can set the packaging project as the startup project and debug as you normally would. It'll build the app and deploy it to a local application. You can see the output within your packaging project's
bin\AnyCPU\<configuration>\AppX directory. It should have more files than your main application as it'll have the self-contained .NET Core runtime in it.
Note: I've sometimes found that debugging the packaging project doesn't cause a rebuild if I've changed certain project files. A rebuild of the main app project has fixed that for me and then I'm debugging what I expect.
There are two main options for deploying the package:
- Sideloading with an AppInstaller file. This is the replacement to ClickOnce.
- The Microsoft Store. The package can be submitted to the Store for distribution.
Since Windows 10 1803, sideloaded applications can receive automatic updates using an
.appinstaller file. This makes AppInstaller a replacement to ClickOnce for most scenarios. The documentation describes how to create this file during publish, so that you can put it on a UNC path, file share, or HTTPS location.
If you sideload, you'll need to use a code signing certificate that's trusted by your users. For an enterprise, that can be a certificate from an internal certificate authority, for the public, it needs to be from a public authority. DigiCert has a great offer for code signing certs, $74/yr for regular and $104/yr for EV at this special link. Disclaimer: DigiCert provides me with free certificates as a Microsoft MVP. I have had nothing but great experiences with them though. Once you have the certificate, you'll need to update your
Package.appxmanifest to use it. Automatic code signing is beyond the scope of this article, but please see my code signing service project for something you can deploy in your organization to handle this.
One of the projects I maintain, NuGet Package Explorer, is a WPF app on .NET Core 3 and is setup with Azure Pipelines. It has a release pipeline that generates a code signed CI build that auto-update, and then promotes packages to the Microsoft Store, Chocolatey, and GitHub. It has a build script that uses Nerdbank.GitVersioning to ensure that each build gets incremented in all the necessary places. I would encourage you to review the project repository for ideas and techniques you may want to use in your own projects.