Every .NET app you have built so far is compiled twice: at build time into the intermediate language IL, and at startup and during the run by the JIT into machine code. That costs startup time, and the image has to bring the whole runtime along. Native AOT (ahead of time) compiles at publish time straight into machine code for one operating system and one processor, takes along only the code that is actually reached, and produces a single executable file with no .NET next to it. Lessons 3.12.4 and before prepared what that requires: source generators instead of reflection. In this lesson you publish a small service with Native AOT, measure what it brings, and decide where it fits in the Kladde and where it doesn't.
New in this lesson:
| Image | what for |
|---|---|
mcr.microsoft.com/dotnet/sdk:10.0-noble-aot |
the SDK with the tools for Native AOT on Linux; for building only |
mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled |
the smallest image to run in: just the system's libraries, no .NET, no shell |
What Native AOT brings and what it costs
What it brings (Microsoft names the same three points):
- Fast startup. No JIT at startup, no runtime that has to be loaded first. That counts where starts are frequent: when scaling up under load, for a service that scales to zero, for command-line tools.
- Smaller images. Only the one file and the system's libraries. Smaller images are pulled faster and have fewer packages that security advisories can come for.
- Less memory, depending on the app: no JIT, no metadata for reflection.
What it costs:
- No code generation at run time.
System.Reflection.Emit, loading assemblies at run time, creating types by name, everything that decides only during the run which code is needed: none of that exists. What the compiler doesn't see at publish time is missing. - Trimming is mandatory. The compiler throws away what it doesn't reach. Code that accesses types through reflection is reported with warnings
(
IL2xxxfor trimming,IL3xxxfor AOT). A warning at publish time means: this spot can fail at run time. WithTreatWarningsAsErrorsas in the Kladde, the build then stops, and that is right. - Not every library. A library that needs reflection doesn't work, or only partly. For every dependency you check whether it supports Native AOT.
- Not every kind of app. In ASP.NET Core, according to Learn: gRPC, health checks, rate limiting, output caching, JWT authentication and static files are supported, minimal APIs partly (with the JSON generator), SignalR partly. Not supported are MVC, Blazor Server, sessions and the other authentication methods. According to its own documentation, EF Core is suitable for Native AOT only to a limited extent and with precompiled queries; read up on the current state on Learn before the course begins.
- One build per platform. A file for
linux-x64runs only on Linux on x64. You build on the target system: on Windows, Native AOT needs the C++ tools of Visual Studio, and for Linux the easiest way is to build in a Linux container. A Native AOT build across operating systems isn't possible. - A longer build. Publishing takes many times as long, because all the code is compiled to machine code.
For the Kladde, that means even before any measurement: the web application with Blazor and EF Core is not a candidate. A small service with a minimal API or gRPC, few dependencies and frequent starts is one.