A feature that was added to .NET Core apps was the ability to publish as a single file. As we approach the release of .NET 5 I thought it worthwhile taking a look at the options for publishing Windows Forms and WPF applications with dotnet publish.
For this post we’re going to work with a very basic Windows Forms and WPF application: WinFormsSingleFileSample and WpfSingleFileSample. To create these applications I used the Windows Forms (WinForms) Application and WPF Application project templates in Visual Studio 16.8 preview 2 (with .NET 5 RC1 installed). After creating the applications I ran them from within Visual Studio just to confirm there was no issue with the creation process – I always recommend doing this otherwise you end up chasing your tail if something is broken and you only realise after doing hours of work.
We’ll start of by calling dotnet publish using the Release configuration (typically the Debug configuration will be used if the Configuration parameter isn’t specified):
dotnet publish -r win-x64 /p:Configuration=Release
Here’s the output for this command. Note that if this is the first time you’ve called dotnet build/publish for a specific architecture (using the
-r win-x64 flag), it may take a few minutes to restore some of the dependencies.
C:\temp\WinFormsSingleFileSample>dotnet publish -r win-x64 /p:Configuration=Release Microsoft (R) Build Engine version 16.8.0-preview-20451-02+51a1071f8 for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore… Restored C:\temp\WinFormsSingleFileSample\WinFormsSingleFileSample\WinFormsSingleFileSample.csproj (in 86 ms). You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview WinFormsSingleFileSample -> C:\temp\WinFormsSingleFileSample\WinFormsSingleFileSample\bin\Release\net5.0-windows\win-x64\WinFormsSingleFileSample.dll WinFormsSingleFileSample -> C:\temp\WinFormsSingleFileSample\WinFormsSingleFileSample\bin\Release\net5.0-windows\win-x64\publish\
The final line includes the output folder for published application. If we look in this folder there are 293 items (280 files and a number of language folders). You can easily zip this folder and copy it to a different machine and run the application but it would be nice to have just a single output.
One option to reduce the number of files to be deployed is to provide the SelfContained parameter. The default value for the SelfContained parameter is true which means that you can copy the output folder to any machine without worrying about whether .NET has been installed.
dotnet publish -r win-x64 /p:Configuration=Release /p:SelfContained=false
Setting SelfContained to false results in a folder containing only 5 items.
Of course, you now need to manage the deployment of .NET dependencies (More information at https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli#self-contained-deployment)
Let’s back up and remove the SelfContained parameter, since we’re really interested in distributing an app and not having to worry about any dependencies. This time we’re going to include the PublishSingleFile parameter
dotnet publish -r win-x64 /p:Configuration=Release /p:PublishSingleFile=true
The output on Windows isn’t exactly a single file. There are 11 files in the output folder – these are the native dependencies required in order to run the application.
For more information on why there are multiple files on Windows, check out the preview 8 blog post by Richard Lander from the dotnet team. This is also covered in the comments of this github issue.
There is still a mechanism where you can achieve a single file output, which is to include the IncludeAllContentForSelfExtract parameter.
dotnet publish -r win-x64 /p:Configuration=Release /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true
The output of this command is two files, one of which is the symbols file (i.e. the pdb file). To deploy this application you can just copy the .exe to the target machine.
One thing to note about the IncludeAllContentForSelfExtract parameter is that when you run the application for the first time, the native dependencies will be extracted which can result in a small delay in launching the app. The managed libraries will be loaded directly from the exe.
Our single file application is currently ~146Mb, which seems a lot for an application that is literally an empty window. Let’s include the PublishedTrimmed file to see if we can optimise the output.
dotnet publish -r win-x64 /p:Configuration=Release /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true /p:PublishTrimmed=true
This command takes a little longer to run but now the output size is just ~83Mb which is a significant improvement. More information on application trimming
The last parameter we’re going to add is PublishReadyToRun. This essentially invokes AOT compilation, which in theory improves startup performance for the app. I say in theory because given the incredibly simple app we’re dealing with here, it’s unlikely we’ll see any actual performance improvement.
dotnet publish -r win-x64 /p:Configuration=Release /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true /p:PublishTrimmed=true /p:PublishReadyToRun=true
Now our application has ballooned back up to ~138Mb.
Hopefully in this post you’ll have seen some of the options available to you when publishing Windows Forms (WinForms) and/or WPF applications.
Note: At the time of writing there is a bug with WPF applications where the PublishSingleFile parameter generates a binary that won’t launch in some cases. According to the dotnet team this is to be fixed with RC2 of .NET 5.
5 thoughts on “Zero Installer, Zero MSIX, Zero Packaging with .NET Single File Apps”
I love how people would rather redeploy 140MB worth of files each time on every app they make instead of setting up .NET once and reducing the size to 150kb :p
It’s another something most people will avoid to use. I have try it and I had problems. Basically it is a self extracting executable that unpacks assemblies into a temporary folder. All good, but because the path of the extracted assembly is not the path of the single file all kind of problems appear. Maybe there are some configuration files not where expected, maybe there are other resources. Maybe there is a windows service assembly. maybe, maybe, maybe. For sure there are solutions for each, but they are not worth it really.
Thank you for your helpful articles, Nick!