Abstract: We practically show examples of 15 different .NET8/9 build modes, including Framework-Dependent and Framework-Independent, how to bundle an app into a single file, how to trim app bundles from unused libraries, and how to build an Ahead-of-time (AOT) precompiled app.
1. .NET 8/9 toolset supports different build/publish/deployment modes
I was experimenting with different project properties/flags, and I developed several proof-of-concept build projects showcasing different build/publish/deployment modes available in .NET8/9, using Microsoft tools.
1.2. Articles in this series
For technical reasons, I will organize this text into several articles.
- .NET 8/9 – Testing different Build/Deployment modes – Part1
- .NET 8/9 – Testing different Build/Deployment modes – Part2
- .NET 8/9 – Testing different Build/Deployment modes – Part3
- .NET 8/9 – Testing different Build/Deployment modes – Part4
- .NET 8/9 – Testing different Build/Deployment modes – Part5
- .NET 8/9 – Testing different Build/Deployment modes – Part6
1.3. Experimenting with Bundling of .NET8/C# application
For the purpose of my project, I started looking into how to deploy some auxiliary tools that I developed as a WinForms/Net8/C# desktop application as a Single-File application, meaning all .dll-s and supporting files are bundled in one assembly.
Over time, I was experimenting more and more with different project properties/flags, and I developed several proof-of-concept build projects showcasing different build/publish/deployment modes available in .NET8, using Microsoft tools.
1.4. Practically oriented projects
I was not really interested in fully understanding and learning all the options available and how they work. I was practically oriented, aiming at some “intermediate level of practical skills” of building recipes and how to properly apply settings and reliably build some applications, resulting in the wanted build/deployment mode.
I think I got it right in most cases. But, for a deeper understanding of the implications of certain project build/publish options, you will need to look elsewhere, like [1], [7].
2. Publish Modes – Terminology reminder
Here is some terminology reminder (very short, see [1] for full formulations)
- Publish Modes – build type
- Debug
- Release
- Publish Modes – regarding installed .NET runtime
- framework-dependent – The app depends on .NET runtime installed
- self-contained (also called framework independent )- The app carries .NET runtime with itself. It does not need to be installed a priori on the deployment machine
- Publish Modes – regarding platform specificity
- Cross-platform – The app can be run on multiple platforms
- Platform-specific – The app targets specific platforms, like X64
- Publish Modes – regarding bundling the app and dependencies
- Un-bundled – The app consists of multiple files
- Single-file (also called bundled )– All app files are bundled into a single file
- Publish Modes – regarding trimming of bundled “Single File” mode
- Untrimmed – During bundling into “Single File" all dependency files are embedded in full content
- Trimmed – During bundling into “Single File" dependency files are examined for dependencies, and only classes/libraries being really needed/used are bundled. Sometimes called tree-shaking or pruning.
- Publish Modes – regarding ReadyToRun (R2R) format. R2R is a form of ahead-of-time (AOT) compilation.
- NotR2R –relay fully on JIT
- ReadyToRun – R2R means that the code is ready to run right away without JIT-ing.
- Publish Modes – regarding if PDB files are separate files
- PdbFilesSeparate –Pdb files are built as separate files
- PdbEmbeded – The PDB file for an assembly can be embedded into the assembly itself (the .dll)
- Publish Modes – regarding if binaries (non-managed files) are included in the single build
- binariesNotInSingleFile –binaries are not included in SingleFile
- binariesInSingleFile – binaries are included in SingleFile
- Publish Modes – regarding if assemblies, when included in SingleFile, are compressed
- assembliesNotCompressed –assemblies in SingleFile are not compressed
- assembliesCompressed – The SingleFile that's produced will have all of the embedded assemblies compressed
- Publish Modes – regarding ahead-of-time (AOT) compilation to native code
- NotAot – not ahead-of-time (AOT) compiled
- PublishAot– ahead-of-time (AOT) compiled to native code
2.1. Comments
- Well, the above list of modes is really my personal interpretation/systematization based on properties/flags that build/publish tools are offering.
- The building technology in question has its own terminology and logic. Common sense reasoning and intuitive hacking, which we all like to do and that saves us time from reading endless documentation, might be misleading in certain situations.
- One funny example of misleading terminology is you need to say “Single-file ” to bundle all assemblies dependencies into a single file, but not all other files are bundled, so you need to tell it “binariesInSingleFile ” in addition to bundling other files too. So, these words above are particular build technology-specific terms and are not always completely intuitive.
- And yes, the original Microsoft documentation [1] .NET 8 uses the term “framework-dependent ”, so do not think that I am here using old-style-terminology. They say.NET8 is no longer a “.NET framework”, but just “.NET”, but here in the documentation [1], that old terminology is still in use. Well, from context, it looks like it means what it used to mean in the old days.
- ReadyToRun format is a bit difficult to grasp. Here are some explanations based on [7]:
- R2R is a form of ahead-of-time (AOT) compilation.
- Ready to run (R2R) is the native executable code format for .NET
- R2R is a binary format for AOT compiled .NET code. R2R means that the code is ready to run right away without jitting. The idea behind the file format is something that can run right away.
- Startup performance is better with R2R; file size is larger with it as it contains the native code in addition to the IL.
- The R2R format is very flexible and supports a range of products ranging from full JIT to full AOT. Currently, R2R is certainly not full AOT, nor is it intended to be, as full native AOT is always limited by the absence of JIT that’s required for e.g. Reflection.Emit or for compiling regexes.
3. .NET 8 not supporting “Framework-dependent Single-file Trimmed” build
When I started this prototyping, what I really had in mind as my goal was Framework-dependent_Single-file_Trimmed build. That would be a build in which all assemblies are bundled into a Single File, unnecessary code/classes would be trimmed, and the resulting single assembly would be .NET framework-dependent. The resulting assembly would be small, compact, tamper-proof, and easy to deploy.
To the best of my knowledge .NET8 tools do not support that build/publish configuration. I was VERY SURPRISED about that situation. The process they call “trimming” is only enabled for “Self-contained”, that is .NET framework installation independent configuration.
I understand all complexities related to statistical lexical analysis and problems that usage of Reflection can cause to the trimming process, sometimes called “pruning” or “tree shaking”.
The thing is that I saw that the build process was working, maybe 10 years ago, for .NET Framework 4.0. It worked well, and I used it regularly. At that time, I was using a RedGate product called SmartAssembly, which is an obfuscator. It had the option to bundle all dependencies into one file and prune/trim unused code. It would then obfuscate the code. I do not know what the status of that tool is now for .NET Core. But, it worked well then, and I was using decompilers, I think it was Reflector then, to look into assembly what was obfuscated, and which classes have not been pruned/ trimmed. It actually does trimming/pruning at the class level, not the assembly level. I think that .NET8 tools do trimming/pruning on the assembly level. There were some limitations then. I remember the file directory tree structure was a problem (for language-dependent .dll-s), but it worked well for my project at that time.
That is why I find it VERY STRANGE that .NET8 build tools, with all the options they have, do not support that particular, very attractive build option. Simply put, I saw such technology 10 years ago in some 3rd-party tools, and is not present today in .NET8.
I was getting this error when I tried that particular build configuration (Framework-dependent_Single-file_Trimmed): Error (active) NETSDK1102 Optimizing assemblies for size is not supported for the selected publish configuration. Please ensure that you are publishing a self-contained app.
4. Project Flags influencing Build/Publish/Deployment modes
The first thing we need are different Project Flags influencing Build/Publish/Deployment modes.
Here is a pretty long list of Project Flags I encountered during my experimenting. I will not pretend that I really understand all of them. Like probably everyone else I was doing a bit of trial-and-error flipping certain flags on and off. But I think it is good to have such a list available as a short overview of syntax if needed.
<!-- OVERVIEW OF BUILD/PUBLISH PROPERTIES IN .csproj FILE -->
<!-- Some special properties in csproj file have syntax like this: -->
<PropertyGroup>
<PropertyXYZ>true</PropertyXYZ>
</PropertyGroup>
<!-- Below listed properties are NOT all of them, and some seem to have changed from .NET6 to .NET8. -->
<!-- When I say changed, I mean meaning/effect and default value if not set. -->
<!-- Still, this is an OVERVIEW of what is available. -->
<!-- For detailed info on usage and effects, check Microsoft latest documentation. -->
<!-- ============================================================= -->
<RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers>
<!-- defines the platforms your app targets, and specify the runtime identifier (RID) of each platform that you target -->
<!-- Specifies the OS and CPU type you're targeting. -->
<!-- ============================================================= -->
<PublishSingleFile>true</PublishSingleFile>
<!-- Enables single file publishing. -->
<!-- ============================================================= -->
<SelfContained>true</SelfContained>
<!-- Determines whether the app is self-contained or framework-dependent. -->
<!-- ============================================================= -->
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<!-- Certain files can be explicitly excluded from being embedded in the single file. -->
<!-- ============================================================= -->
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<!-- The single file that's produced will have all of the embedded assemblies compressed, which can significantly reduce the size of the executable. -->
<!-- ============================================================= -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<!-- Only managed DLLs are bundled with the app into a single executable by default, but the native binaries of the core runtime itself are separate files. -->
<!-- To embed native binaries for extraction and get one output file, set the property. -->
<!-- ============================================================= -->
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<!-- Only managed DLLs are bundled with the app into a single executable by default, but the native binaries are separate files. -->
<!-- To embed all files, including native binaries for extraction and get one output file, set the property. -->
<!-- ============================================================= -->
<DebugType>embedded</DebugType>
<!-- The PDB file for an assembly can be embedded into the assembly itself (the .dll). -->
<!-- ============================================================= -->
<PublishReadyToRun>true</PublishReadyToRun>
<!-- Regarding ReadyToRun (R2R) format. R2R is a form of ahead-of-time (AOT) compilation. -->
<!-- ============================================================= -->
<PublishReadyToRunExclude Include="Contoso.Example.dll" />
<!-- Exclude specific assemblies from ReadyToRun processing. -->
<!-- ============================================================= -->
<PublishReadyToRunEmitSymbols>true</PublishReadyToRunEmitSymbols>
<!-- Symbol generation for use with profilers. -->
<!-- ============================================================= -->
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
<!-- Composite ReadyToRun compiles a set of assemblies that must be distributed together. -->
<!-- This has the advantage that the compiler is able to perform better optimizations and reduces the set of methods that cannot be compiled via the ReadyToRun process. -->
<!-- ============================================================= -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Will produce a trimmed app on self-contained publish. -->
<!-- ============================================================= -->
<TrimMode>full</TrimMode>
<!-- Set the trimming granularity to either partial or full. -->
<!-- ============================================================= -->
<TrimmableAssembly Include="MyAssembly" />
<!-- Opt-in individual assemblies to trimming. -->
<!-- ============================================================= -->
<TrimmerRootDescriptor Include="MyRoots.xml" />
<!-- Specify roots for analysis using an XML file that uses the trimmer descriptor format. -->
<!-- This lets you root specific members instead of a whole assembly. -->
<!-- ============================================================= -->
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<!-- Trimming removes IL that's not statically reachable. Apps that use reflection or other patterns that create dynamic dependencies might be broken by trimming. -->
<!-- To warn about such patterns, set <SuppressTrimAnalysisWarnings> to false. -->
<!-- ============================================================= -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<!-- Enable a Roslyn analyzer for a subset of trim analysis warnings. -->
<!-- ============================================================= -->
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
<!-- Don't treat ILLink warnings as errors. -->
<!-- ============================================================= -->
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<!-- Show all detailed warnings, instead of collapsing them to a single warning per assembly. -->
<!-- ============================================================= -->
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<!-- Remove symbols from the trimmed application, including embedded PDBs and separate PDB files. -->
<!-- ============================================================= -->
<IsTrimmable>true</IsTrimmable>
<!-- Setting the MSBuild property IsTrimmable to true marks the assembly as "trimmable" and enables trim warnings. -->
<!-- ============================================================= -->
<PublishAot>true</PublishAot>
<!-- This property enables Native AOT compilation during publish. It also enables dynamic code-usage analysis during build and editing. -->
<!-- ============================================================= -->
<IsAotCompatible>true</IsAotCompatible>
<!-- Indicate whether a library is compatible with Native AOT. -->
<!-- ============================================================= -->
<StripSymbols>false</StripSymbols>
<!-- On Unix-like platforms, set the StripSymbols property to false to include the debug information in the native binary. -->
<!-- ============================================================= -->
<_SuppressWinFormsTrimError>true</_SuppressWinFormsTrimError>
<!-- Strange, this one is not documented and I see people on the internet using it. -->
5. Dotnet Publish command
The next thing we need is the “dotnet publish” command. Here is the help file.
======================================================
>dotnet publish /?
Description:
Publisher for the .NET Platform
Usage:
dotnet publish [<PROJECT | SOLUTION>...] [options]
Arguments:
<PROJECT | SOLUTION> The project or solution file to operate on.
If a file is not specified, the command will search
the current directory for one.
Options:
--ucr, --use-current-runtime Use current runtime as the target runtime.
-o, --output <OUTPUT_DIR> The output directory to place the published artifacts in.
--artifacts-path <ARTIFACTS_DIR> The artifacts path. All output from the project, including build, publish,
and pack output, will go in subfolders under the specified path.
--manifest <MANIFEST> The path to a target manifest file that contains the list of packages
to be excluded from the publish step.
--no-build Do not build the project before publishing. Implies --no-restore.
--sc, --self-contained Publish the .NET runtime with your application so the runtime doesn't need
to be installed on the target machine.
The default is 'false.' However, when targeting .NET 7 or lower, the default is
'true' if a runtime identifier is specified.
--no-self-contained Publish your application as a framework-dependent application. A compatible .NET
runtime must be installed on the target machine to run your application.
--nologo Do not display the startup banner or the copyright message.
-f, --framework <FRAMEWORK> The target framework to publish for. The target framework has to be specified
in the project file.
-r, --runtime <RUNTIME_IDENTIFIER> The target runtime to publish for. This is used when creating a self-contained
deployment.
The default is to publish a framework-dependent application.
-c, --configuration <CONFIGURATION> The configuration to publish for. The default is 'Release' for NET 8.0 projects
and above, but 'Debug' for older projects.
--version-suffix <VERSION_SUFFIX> Set the value of the $(VersionSuffix) property to use when building the project.
--interactive Allows the command to stop and wait for user input or action (for example to
complete authentication).
--no-restore Do not restore the project before building.
-v, --verbosity <LEVEL> Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal],
n[ormal], d[etailed], and diag[nostic].
-a, --arch <ARCH> The target architecture.
--os <OS> The target operating system.
--disable-build-servers Force the command to ignore any persistent build servers.
-?, -h, --help Show command line help.
============================================================
6. To be continued
This will be continued in the next article of the series.
7. References
[1] .NET application publishing overview https://learn.microsoft.com/en-us/dotnet/core/deploying/
[2] Self-contained deployment runtime roll forward https://learn.microsoft.com/en-us/dotnet/core/deploying/runtime-patch-selection
[3] https://www.red-gate.com/products/smartassembly/
[7] https://devblogs.microsoft.com/dotnet/conversation-about-ready-to-run/