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. .NET8/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.
- .NET8/9 – Testing different Build/Deployment modes – Part1
- .NET8/9 – Testing different Build/Deployment modes – Part2
- .NET8/9 – Testing different Build/Deployment modes – Part3
- .NET8/9 – Testing different Build/Deployment modes – Part4
- .NET8/9 – Testing different Build/Deployment modes – Part5
- .NET8/9 – Testing different Build/Deployment modes – Part6
2. Some Tricks
Here are some useful topics.
2.1 Embedding.PDB files
Embedding .pdb files into your assemblies is possible and will give you the line number of the exception.
/*
Without PDB files there are no line numbers in the Exception log
*/
public static string? ExceptionTest()
{
string? result = null;
try
{
throw new Exception("Exception_from_ClassLibraryA.ClassA");
}
catch (Exception ex)
{
result = ex.ToString();
}
return result;
}
/*
====Exception Log without PDB files=============
System.Exception: Exception_from_ClassLibraryA.ClassA
at ClassLibraryA.ClassA.ExceptionTest()
====Exception Log with PDB files===============
System.Exception: Exception_from_ClassLibraryA.ClassA
at ClassLibraryA.ClassA.ExceptionTest() in C:\tmpNetBundle\BundleExample01\ClassLibrary1\ClassA.cs:line 18
==This is from AOT compiled project (build 19), with .pdb files==============
System.Exception: Exception_from_ClassLibraryA.ClassA
at ClassLibraryA.ClassA.ExceptionTest() + 0x31
*/
2.2 Some APIs change behavior in different build/deployment modes.
Some APIs aren't compatible with single-file deployment (see [4]). Here, I test assembly location one, which is of interest to me, and show its substitute.
/*
Some assembly location API behave differently in SingleFile bundled build
-API System.Reflection.Assembly.GetExecutingAssembly() will generally work
-but System.Reflection.Assembly.GetExecutingAssembly().Location no longer works
*/
static string? TestAssemblyLocationAPI()
{
string? result = String.Empty;
string ? locationAssembly =
System.Reflection.Assembly.GetExecutingAssembly().Location;
//result += "\n";
result +=$"locationAssembly:{locationAssembly}";
string? BaseDirectory = System.AppContext.BaseDirectory;
result += "\n";
result += $"BaseDirectory:{BaseDirectory}";
string? ProcessPath = System.Environment.ProcessPath;
result += "\n";
result += $"ProcessPath:{ProcessPath}";
result += "\n";
return result;
}
/*
===Execution in regular build==================
locationAssembly:C:\tmpNetBundle\BundleExample01\ConsoleApp1\bin\Debug\net8.0-windows\win-x64\ConsoleApp1.dll
BaseDirectory:C:\tmpNetBundle\BundleExample01\ConsoleApp1\bin\Debug\net8.0-windows\win-x64\
ProcessPath:C:\tmpNetBundle\BundleExample01\ConsoleApp1\bin\Debug\net8.0-windows\win-x64\ConsoleApp1.exe
===Execution in SingleFile bundled build==================
locationAssembly:
BaseDirectory:C:\tmpNetBundle\BundleExample01\ConsoleApp1\Framework_SingleFile_Win-X64\
ProcessPath:C:\tmpNetBundle\BundleExample01\ConsoleApp1\Framework_SingleFile_Win-X64\ConsoleApp1.exe
*/
3. Sample Test App
I created a sample Test App for this prototyping.
It consists of
- a library ClassLibraryA that has EF8 Database access in it.
- Executable ConsoleApp1 that invokes some, but not all, API exposed in the library so I can test “trimming”. The process of statistical lexical analysis should discover that the main app is not really using database access and that all linked .dll-s related to EF8/SQL are not needed.
- Because many properties/flags are set on the project level, I need to create several clones of ConsoleApp1 to test all the variations in project properties/flags.
- I was using PostBuild Event to run “dotnet publish” scripts. (Advice: be sure to add enough spaces after each command, before ==, after ==, etc.)
// ClassLibraryA, ClassA.cs =========================================
using ClassLibraryA.EF_Work;
namespace ClassLibraryA
{
public static class ClassA
{
public static string? BasicCall()
{
string? result = "Hello from ClassLibraryA.ClassA";
return result;
}
public static string? ExceptionTest()
{
string? result = null;
try
{
throw new Exception("Exception_from_ClassLibraryA.ClassA");
}
catch (Exception ex)
{
result = ex.ToString();
}
return result;
}
public static string? BasicDbTest()
{
string? result = EF_Test1.BasicDbTest();
return result;
}
}
}
// ConsoleApp1, Program.cs ===============================================
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World from ConsoleApp1\n");
Console.WriteLine(TestAssemblyLocationAPI());
Console.WriteLine(ClassLibraryA.ClassA.BasicCall());
Console.WriteLine(ClassLibraryA.ClassA.ExceptionTest());
// deliberately commented to test Trim mode
// Console.WriteLine(ClassLibraryA.ClassA.BasicDbTest());
Console.ReadLine();
}
static string? TestAssemblyLocationAPI()
{
string? result = string.Empty;
string? locationAssembly =
System.Reflection.Assembly.GetExecutingAssembly().Location;
result += $"locationAssembly: {locationAssembly}";
string? BaseDirectory = System.AppContext.BaseDirectory;
result += "\n";
result += $"BaseDirectory: {BaseDirectory}";
string? ProcessPath = System.Environment.ProcessPath;
result += "\n";
result += $"ProcessPath: {ProcessPath}";
result += "\n";
return result;
}
}
}
4. Build properties/flags variations
I finished with a number of build variations since I wanted to test different options.
Here are all properly documented.
<!-- NOTE: THIS_SOLUTION_STOPPED_WORKING_FOR_NEWER_NET_VERSIONS -->
<!-- These project settings worked well around .NET 8.0.0. Later upgrades to .NET 8.0+ and .NET 9.0 introduced breaking changes in the build tools. -->
===Library-ClassLibraryA-Start===========================================
===ClassLibraryA.csproj==================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<Platforms>AnyCPU;x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<DebugType>embedded</DebugType>
<PublishTrimmed>true</PublishTrimmed>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
</Project>
===Library-ClassLibraryA-End===========================================
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
===Project ConsoleApp1 - Start========================================
===ConsoleApp1.csproj==================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<Platforms>AnyCPU;x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<DebugType>embedded</DebugType>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
</Project>
===ConsoleApp1.csproj - PostBuildEvent==================================
echo +++Post-Build+++++
if $(ConfigurationName) == Debug (
echo +++++Debug+++++
)
if $(ConfigurationName) == Release (
echo +++++Framework_SingleFile_Win-X64.cmd+++++
call Framework_SingleFile_Win-X64.cmd
)
===Framework_SingleFile_Win-X64.cmd==================================
dotnet publish ConsoleApp1.csproj --no-build --runtime win-x64 --use-current-runtime --configuration Release -p:PublishSingleFile=true --no-self-contained --output ./Framework_SingleFile_Win-X64
==Builds================
Location: c:\tmpNetBundle\BundleExample01\ConsoleApp1\bin\Release\net8.0-windows\win-x64\
Publish mode: Release; framework-dependent; Platform-specific-win-x64; Untrimmed; NotR2R; PdbEmbedded; binariesNotInSingleFile; assembliesNotCompressed; NotAot
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
===Project ConsoleApp2 - Start========================================
===ConsoleApp2.csproj==================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<Platforms>AnyCPU;x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<DebugType>embedded</DebugType>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<IsTrimmable>true</IsTrimmable>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>
===ConsoleApp2.csproj - PostBuildEvent==================================
echo +++Post-Build+++++
if $(ConfigurationName) == Debug (
echo +++++Debug+++++
)
if $(ConfigurationName) == Release (
echo +++++SelfContained_SingleFile_win-x64.cmd+++++
call SelfContained_SingleFile_win-x64.cmd
echo +++++SelfContained_SingleFile_win-x64_Trimmed.cmd+++++
call SelfContained_SingleFile_win-x64_Trimmed.cmd
)
===SelfContained_SingleFile_win-x64.cmd==================================
dotnet publish ConsoleApp2.csproj --no-build --runtime win-x64 --configuration Release -p:PublishSingleFile=true -p:SelfContained=true -p:PublishReadyToRun=false -p:PublishTrimmed=false --output ./SelfContained_SingleFile_win-x64
===SelfContained_SingleFile_win-x64_Trimmed.cmd==================================
dotnet publish ConsoleApp2.csproj --no-build --runtime win-x64 --configuration Release -p:PublishSingleFile=true -p:SelfContained=true -p:PublishReadyToRun=false -p:PublishTrimmed=true --output ./SelfContained_SingleFile_win-x64_Trimmed
==Builds================
Location: c:\tmpNetBundle\BundleExample01\ConsoleApp2\bin\Release\net8.0-windows\win-x64\
Publish mode: Release; self-contained; Platform-specific-win-x64; Untrimmed; NotR2R; PdbEmbedded; binariesNotInSingleFile; assembliesNotCompressed; NotAot
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
<!-- The remaining projects (ConsoleApp3 and ConsoleApp1C) follow the same structured formatting. -->
Note. THIS_SOLUTION_STOPPED_WORKING_FOR_NEWER_NET_VERSIONS
NOTE. These project settings all worked well somewhere around .NET 8.0.0. Later, with the upgrade of .NET runtime to later versions of .NET 8.0 and .NET 9.0 and an upgrade to Visual Studio, some of those projects stopped working. It looks like they introduced breaking changes in the build tools. Logic is still sound and build types are the same, just the build tools started to behave a bit differently. New build configurations and build scripts are needed.
5. To be continued
This will be continued in the next article of the series.
6. 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/
[4] https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli#api-incompatibility