PublishSingleFile bundling, framework-dependent versus self-contained, native library extraction, Assembly.Location and IL3000.
<PublishSingleFile>true</PublishSingleFile> bundles the app's managed assemblies (and, if self-contained, the runtime) into one executable for a given runtime identifier. Managed assemblies are loaded directly from the bundle; native libraries stay next to the executable unless IncludeNativeLibrariesForSelfExtract is set, in which case they are extracted to a temp folder at startup. It is still a JIT app, unlike NativeAOT. The main breakage: Assembly.Location returns an empty string, so code that finds files relative to the DLL fails; use AppContext.BaseDirectory instead. The analyser warns with IL3000.
Single-file publish bundles an application — its managed assemblies, configuration files, and optionally native libraries and the runtime — into one executable file. It's a deployment packaging feature: the app still runs on the normal CoreCLR with JIT (unlike NativeAOT). What breaks is mostly code that assumes assemblies are separate files on disk.
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <!-- include the runtime (optional) -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile> <!-- self-contained only -->
</PropertyGroup>
Variants:
PublishTrimmed) and ReadyToRun to reduce size and improve start-up.IncludeNativeLibrariesForSelfExtract), or with self-contained apps on some platforms, statically linked runtime components.ExcludeFromSingleFile) to stay alongside the exe (e.g. config files you want editable).Assembly.Location is empty
var dir = Path.GetDirectoryName(typeof(Program).Assembly.Location); // "" in single-file -> bugs
var dir2 = AppContext.BaseDirectory; // correct: folder of the executable
Code that locates files relative to the assembly (plugins, templates, config, Assembly.Location, Assembly.CodeBase, AppDomain.BaseDirectory assumptions) must use AppContext.BaseDirectory. The analyser IL3000 warns about Assembly.Location usage.
Assembly.LoadFrom / probing the app folder for DLLs: bundled assemblies aren't files; code scanning the directory for *.dll finds nothing.
Module.FullyQualifiedName, Assembly.GetFile(s), Assembly.CodeBase: return placeholders or throw (IL3001/IL3002).
Native dependencies: libraries that DllImport native DLLs by relative path may fail unless native libraries are next to the exe or extracted; extraction requires a writable temp directory (DOTNET_BUNDLE_EXTRACT_BASE_DIR), which can be a problem in locked-down environments.
Resources and content files: files marked CopyToOutputDirectory need explicit handling (IncludeAllContentForSelfExtract or excluding them from the bundle) — code reading them from disk may break.
Reflection/serialisers using file paths: XML serializer generated assemblies, some plugin frameworks, Razor runtime compilation.
Debugging/diagnostics: PDBs are separate by default (or embedded); some profilers or tools that read assemblies from disk may struggle.
Antivirus/start-up: self-extracting single files can trigger AV scans or be slower on first run.
Size: self-contained single files are large (60–100+ MB) unless trimmed.
Less useful for containers (the image already packages files) and web apps behind CI/CD pipelines.
An internal CLI tool moved to single-file self-contained publish so ops staff could run it without installing .NET. It immediately failed to find its templates/ folder: the code used Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), now an empty string, so it searched the current working directory. Switching to AppContext.BaseDirectory, excluding the template folder from the bundle, and enabling the single-file analysers (IL3000) fixed it; we also enabled trimming and compression to cut the file from ~70 MB to ~25 MB after fixing trim warnings.
The interviewer checks practical deployment knowledge.
Green flags: bundles managed assemblies, needs RID, framework-dependent versus self-contained, native library extraction, Assembly.Location empty and AppContext.BaseDirectory, not the same as AOT.
Red flags: confuses single-file with NativeAOT; unaware of path breakage.
The apphost executable is followed by bundled files and a manifest (file names, offsets, sizes, compression flags), with a marker at a known offset. At start-up, the host reads the manifest, maps the bundle, and provides the runtime with in-memory assembly images through a bundle probe callback.
CoreCLR can load assemblies from memory-mapped regions of the bundle. Assembly.Location returns empty because there's no path; Assembly.GetName().CodeBase is not meaningful. AppContext.BaseDirectory points to the executable's directory.
Setting PublishSingleFile (or EnableSingleFileAnalyzer) enables warnings:
Assembly.Location returns empty in single-file.Assembly.GetFile(s) throws.[RequiresAssemblyFiles].Libraries can mark APIs with [RequiresAssemblyFiles] to propagate warnings.
PublishTrimmed reduces size (requires trim-compatible code).PublishReadyToRun improves start-up (larger size).EnableCompressionInSingleFile reduces size at the cost of decompression at start-up (self-contained only).Assembly.Location and AppContext.BaseDirectory.IncludeNativeLibrariesForSelfExtract and observe the extraction directory.using System.Reflection;
// Empty string when running as a single-file bundle (and analyser warning IL3000)
#pragma warning disable IL3000
string location = typeof(Program).Assembly.Location;
#pragma warning restore IL3000
Console.WriteLine($"Assembly.Location: '{location}'");
Console.WriteLine($"AppContext.BaseDirectory: '{AppContext.BaseDirectory}'");
Console.WriteLine($"Environment.ProcessPath: '{Environment.ProcessPath}'");
// Correct way to find files shipped next to the app
string templates = Path.Combine(AppContext.BaseDirectory, "templates");
Console.WriteLine($"Templates folder exists: {Directory.Exists(templates)}");
partial class Program;
<PropertyGroup>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
Assembly.Location for pathsWhat happens: locating files relative to the assembly.
Why it is wrong: empty string in single-file apps.
Do instead: AppContext.BaseDirectory.
What happens: Directory.GetFiles(baseDir, "*.dll").
Why it is wrong: bundled assemblies aren't files.
Do instead: register components explicitly or ship plugins separately.
What happens: native DLLs not found at run time. Why it is wrong: not bundled/extracted by default. Do instead: configure self-extraction or deploy alongside.
What happens: self-extraction fails in locked-down environments.
Why it is wrong: extraction needs a writable directory.
Do instead: set DOTNET_BUNDLE_EXTRACT_BASE_DIR or avoid extraction.
What happens: expecting native performance and no runtime. Why it is wrong: single-file still uses CoreCLR and JIT. Do instead: choose AOT for those benefits.
What happens: distributing 80 MB self-contained files. Why it is wrong: slow downloads. Do instead: trimming and compression where compatible.
What happens: appsettings.json embedded in the bundle.
Why it is wrong: can't be edited after deployment.
Do instead: exclude config from the bundle.
What happens: extra build complexity in containers. Why it is wrong: little benefit; images already package files. Do instead: use standard publish for containers.
Single-file publish is mostly a distribution convenience.
| Goal | Option |
|---|---|
| One file to copy for a CLI or desktop tool | Single-file, self-contained |
| Small container image | Framework-dependent, not single-file |
| Fast startup | ReadyToRun or NativeAOT |
| No runtime on target, smallest size | Single-file + trimming, or NativeAOT |
Seniors turn on the single-file analyser early and fix path handling before anyone ships a broken tool.
NativeAOT (<PublishAot>true</PublishAot>) compiles the whole app, with the parts of the runtime it needs, into one native executable with no JIT and no IL at run time. You get very fast startup, lower memory and a smaller deployment with no .NET runtime to install. The cost is a closed world: trimming is mandatory, Reflection.Emit and loading new assemblies at run time are not available, reflection works only on what the compiler can see, and libraries must be AOT-compatible. ASP.NET Core supports it for minimal APIs and gRPC but not MVC or Razor Pages. Fix the IL2xxx and IL3xxx warnings before shipping.
Trimming (<PublishTrimmed>true</PublishTrimmed>, self-contained only, and always on for NativeAOT) runs the ILLink trimmer at publish to remove types and members that static analysis proves are unreachable, shrinking the app. Reflection is its enemy: code that finds types or members by string at run time may lose them. The trimmer reports this with warnings such as IL2026 (RequiresUnreferencedCode) and IL2070-series (missing DynamicallyAccessedMembers). You make code trim-safe by using source generators instead of reflection, annotating Type parameters with [DynamicallyAccessedMembers], and marking unavoidable reflection with [RequiresUnreferencedCode]. Libraries declare <IsTrimmable>true</IsTrimmable>.
By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.
How to tell it (STAR)
We moved [our scaffolding CLI] to single-file and it broke for everyone. I traced it to Assembly.Location being empty inside the bundle, switched to AppContext.BaseDirectory and made IL3000 a build error so it could not regress.
Last updated 31 Oct 2026.