Runtime Identifiers and Native AOT
A Runtime Identifier (RID), strings like win-x64, linux-x64, linux-musl-x64, or osx-arm64, tells the .NET build and publish tooling exactly which operating system and CPU architecture a build output targets. RIDs matter whenever platform-specific native assets are involved: restoring a NuGet package that ships native binaries, publishing a self-contained deployment, or compiling with Native AOT all require a concrete RID so the tooling knows which native runtime pack to pull in. A 'portable' RID like linux-x64 targets glibc-based distributions broadly, while linux-musl-x64 specifically targets musl-libc distributions like Alpine.
Cricket analogy: A cricket kit bag is packed differently for a tour of England (swing-friendly Dukes ball, heavier sweaters) versus a tour of the UAE (dry pitches, sunscreen), just as a RID like win-x64 or linux-arm64 tells the build exactly which 'conditions' to pack native binaries for.
Native AOT: Compiling Straight to Native Code
Native AOT (dotnet publish -p:PublishAot=true) compiles an application directly to a single native, platform-specific executable ahead of time, completely bypassing the JIT compiler at runtime. Because there's no just-in-time compilation or tiered warm-up, a Native AOT app reaches full execution speed immediately on startup and typically launches in a fraction of the time of a JIT-based app, with a noticeably smaller memory footprint since the full JIT compiler and much of the metadata-driven reflection machinery aren't included in the output.
Cricket analogy: A T20 opener like Jos Buttler skips the slow build-up of a Test innings and attacks from ball one, similar to Native AOT skipping the JIT's warm-up entirely by compiling straight to native code so the app is at full speed from the first request.
Self-Contained vs Framework-Dependent Deployments
Self-contained deployments (dotnet publish -r linux-x64 --self-contained) bundle the entire .NET runtime for a specific RID alongside the application, so the target machine doesn't need any .NET runtime pre-installed, but the resulting output is RID-specific and won't run on a different OS/architecture combination without republishing. Framework-dependent deployments, by contrast, are smaller because they rely on a compatible shared runtime already installed on the target machine, resolved at launch time rather than bundled at publish time.
Cricket analogy: A touring cricket team can either bring its own full support staff and equipment (self-contained) or rely on the host board's facilities and physios (framework-dependent), just as a self-contained .NET deployment bundles its own runtime per RID versus a framework-dependent one relying on a shared installed runtime.
# Publish a Native AOT single executable targeting Alpine Linux (musl)
dotnet publish -c Release -r linux-musl-x64 -p:PublishAot=true
# csproj settings that make this viable
# <PropertyGroup>
# <PublishAot>true</PublishAot>
# <InvariantGlobalization>true</InvariantGlobalization>
# </PropertyGroup>Constraints and Trade-offs of Native AOT
Native AOT's static compilation model can't accommodate code that depends on generating IL at runtime or resolving types dynamically, so APIs like System.Reflection.Emit, some forms of runtime code generation, and reflection-based JSON serialization aren't supported without extra work. The IL linker (trimmer) built into the Native AOT pipeline performs aggressive static analysis to determine which code is actually reachable, and it will trim away anything it can't prove is used, which means reflection-only code paths need to be explicitly preserved with attributes like DynamicallyAccessedMembers, or replaced with source-generated alternatives such as the System.Text.Json source generator.
Cricket analogy: A pitch that only supports pace bowling and can't handle a specialist leg-spinner's footholds forces a captain to adapt the bowling attack, similar to Native AOT not supporting dynamic reflection-based code paths, forcing developers toward source generators instead.
Choosing between linux-x64 and linux-musl-x64 matters for containers: Alpine-based Docker images use musl libc, not glibc, so a self-contained or Native AOT publish targeting the wrong RID will fail to run inside an Alpine container even though the RID 'sounds' close enough.
Native AOT does not support reflection-based JSON serialization via System.Text.Json by default. You must opt into the source-generated JsonSerializerContext instead, or the app will throw at runtime for types the trimmer couldn't statically resolve.
- A Runtime Identifier (RID) like linux-x64 or osx-arm64 specifies the target OS and CPU architecture for a build.
- Native AOT compiles straight to a native executable ahead of time, eliminating JIT warm-up for near-instant startup.
- Self-contained deployments bundle the runtime per RID; framework-dependent deployments rely on a preinstalled shared runtime.
- linux-musl-x64 targets musl-libc distributions like Alpine, distinct from the glibc-based linux-x64.
- Native AOT's trimmer statically analyzes reachable code and removes anything it can't prove is used.
- Reflection-heavy code (Reflection.Emit, dynamic JSON serialization) needs source generators or explicit annotations to work under Native AOT.
- Choosing the correct RID at publish time is required for both self-contained deployments and Native AOT builds.
Practice what you learned
1. What does the RID `linux-musl-x64` specifically target?
2. What is the primary runtime-level difference of a Native AOT-published app compared to a normal .NET app?
3. What is a key drawback of a self-contained deployment compared to a framework-dependent one?
4. Why might reflection-based JSON serialization fail under Native AOT?
5. Which attribute helps preserve reflection-dependent members from being trimmed away under Native AOT?
Was this page helpful?
You May Also Like
.NET on Linux and macOS
How the .NET SDK and runtime install, behave, and get deployed across Linux and macOS, and the platform differences that matter for portable code.
Publishing Single-File Apps
How .NET's single-file publishing bundles an app and its dependencies into one executable, how extraction and trimming work, and what caveats to expect.
Global and Local Tools
How to install, pin, build, and manage .NET CLI tools, both globally on a machine and locally per repository via a tool manifest.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics