.NET Core Standalone Executable

.NET Core 1.0 came out June 27, 2016. 4 years later, and who knows how many hundreds of thousands of person hours development, I figured it would be quite mature.

On that premise, feeling quite hopeful, I decided to see what’s involved in converting a .NET 4.7.1 standalone console application to .NET Core 3.1, which you’d think would be relatively straight forward.

Three hours later, my 5MB standalone console application has ballooned to 74MB! If you select ‘PublishTrimmed=true’, then the size drops to 44MB but then the application doesn’t work. Apparently, trimming is not able to work out what’s needed, even when reflection isn’t involved.

Turns out even the un-trimmed 74MB app. still doesn’t work as you can’t use the built-in encrypted connection strings section in app.config file. (It hasn’t currently been implemented in .NET Core, along with DbProviderFractory, and a few other surprises…)

I went looking for resources and other people’s experiences converting to .NET Core.

https://docs.microsoft.com/en-us/dotnet/core/porting/
https://docs.microsoft.com/en-us/dotnet/standard/analyzers/api-analyzer
https://github.com/hvanbakel/CsprojToVs2017
https://ianqvist.blogspot.com/2018/01/reducing-size-of-self-contained-net.html

Scott Hanselman gets really excited about making a 13MB+ “Hello world” .Net Core application. He even calls it tiny!! (and that’s after he got it down from 69MB). His post starts out with the line “I’ve always been fascinated by making apps as small as possible, especially in the .NET space.” Irony, or what? In what kind of insane world is a “Hello World!” application 13MB!?!

On a tangential side note; just ditched ILMerge for creating standalone executables. In the past I’ve used Jeffrey Richter’s technique of embedding assemblies in the resource manifest, adding a startup hook to load assemblies into the app. domain at runtime, but like a FOOL, I thought that ILMerge was the ‘better’, more .NETway of doing things.

The amount of pain ILMerge has caused me over the last few years is staggering. It has to be one of the most fragile tools out there. If the planets aren’t aligned it spits the dummy. If there’s ever a problem it spits out an unhelpful cryptic “exited with error X” message. Good luck finding the problem!

Just moved over to using Fody/Costura; it uses that same technique of embedding assemblies in the executable.

It worked the very first time! Unlike ILMerge. As an added bonus it automatically compresses/decompresses assemblies, and my .NET 4.7.1 standalone executable is 2 MB smaller!

Create a Single .NET Executable with Multiple Assemblies

If you want to create a single .NET executable there are several possibilities. In the past I’ve used Jeffrey Richter’s technique of embedding DLLs as resources and loading these into memory from the Manifest Resource Stream on startup. While this works well, it does require a bit of extra code to not try to access anything in an assembly until after it’s been loaded at startup.

In the interest of cleaner code, I decided to give ILMerge a go.

It’s simple to install ILMerge into your application using NuGet:

PM> Install-Package ilmerge -Version 2.14.1208

To run ILMerge on your release builds, add an AfterBuild section to your default projects .csproj file:

  <Target Name="AfterBuild" Condition=" '(Configuration)' == 'Release' ">     <CreateItem Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)'=='.dll'">       <Output ItemName="AssembliesToMerge" TaskParameter="Include" />     </CreateItem>     <PropertyGroup>       <ReferenceAssemblies>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2</ReferenceAssemblies>     </PropertyGroup>     <Message Importance="high" Text="Executing ILMerge...with target platform from(ReferenceAssemblies)" />
    <Exec Command="&quot;(SolutionDir)\packages\ILMerge.2.14.1208\tools\ILMerge.exe" /target:exe /out:@(MainAssembly) /internalize /targetplatform:v4,"(ReferenceAssemblies)&quot; &quot;@(IntermediateAssembly)&quot; @(AssembliesToMerge->'&quot;%(FullPath)&quot;', ' ')" />
    <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" />
  </Target>

Note: If you receive this error from ILMerge when building your release executable:

An exception occurred during merging:
ILMerge.Merge: There were errors reported in AWSSDK.Core’s metadata.
Array dimensions exceeded supported range.
at ILMerging.ILMerge.Merge()
at ILMerging.ILMerge.Main(String[] args)

It’s because ILMerge doesn’t currently support the new portable PDB format.
https://github.com/Microsoft/ILMerge/issues/29
https://github.com/Microsoft/ILMerge/issues/11

If you can live without the .pdb files being merged into a single PDB file add /ndebug to the Exec command.

This is a temporary workaround for issues with ILMerge where it cannot process the new portable .NET PDB format.

An excellent resource on PDB files is John Robbins’s : PDB Files: What Every Developer Must Know :

A .NET PDB only contains two pieces of information, the source file names and their lines and the local variable names. All the other information is already in the .NET metadata so there is no need to duplicate the same information in a PDB file.