ASP.NET Core applications traditionally run on the .NET runtime, which loads assemblies and uses runtime features such as reflection, dynamic code generation, and just-in-time compilation. This model works well for most web services, but it can introduce startup work and deployment requirements that are unnecessary for some applications.
Native Ahead-of-Time (AOT) compilation offers another approach. It compiles an application into native machine code during publishing, allowing the resulting executable to start without compiling its application code through the JIT at runtime.
For teams building APIs, background-facing HTTP services, and containerized workloads, Native AOT can reduce startup time, lower memory consumption in suitable workloads, and simplify deployment by reducing runtime dependencies.
However, it is not a universal performance switch. Native AOT introduces restrictions around reflection, dynamic code generation, and library compatibility. Developers need to evaluate these constraints before choosing it for an existing ASP.NET Core application.
This article explains how to publish a minimal ASP.NET Core service using Native AOT, how to validate the generated executable, and what to consider before deploying it to production.
What Is Native AOT in .NET?
Native AOT compiles managed application code into native machine code before the application runs. The published executable contains the compiled application and the required runtime components.
With a traditional framework-dependent deployment, an application generally requires a compatible .NET runtime on the target machine. JIT compilation converts methods into native machine code as they execute.
Native AOT changes that execution model. The compiler produces native code during the publishing process, so the application does not depend on runtime JIT compilation for its compiled application methods.
The resulting executable can offer several advantages:
Faster startup: The application avoids much of the JIT compilation work normally performed during startup and execution.
Lower memory usage: Some workloads use less memory because of differences in runtime behavior and application composition.
Simplified deployment: The published application can run without a separately installed .NET runtime when the deployment includes the necessary native dependencies.
Reduced deployment footprint: Trimming and ahead-of-time compilation can remove code that the application does not require.
These benefits depend on the application's workload, dependencies, and deployment environment. Native AOT can also increase build time and produce larger executables than some alternative deployment configurations.
How Native AOT Works with ASP.NET Core
ASP.NET Core includes runtime features that rely on metadata inspection and dynamic behavior. Examples include reflection-based serialization, dependency injection patterns, and frameworks that discover types or methods at runtime.
Native AOT requires the compiler to determine which code and metadata must be included in the final executable. It cannot support every dynamic runtime pattern in the same way as a regular JIT-based application.
ASP.NET Core supports Native AOT for specific application models, particularly Minimal APIs. These applications use explicit route definitions and can benefit from compile-time code generation and reduced reflection.
For example, an API with a small number of routes and a straightforward dependency graph is generally easier to adapt than a large application built around extensive runtime discovery.
Native AOT is not a direct replacement for every ASP.NET Core hosting model. Traditional MVC controllers, Razor Pages, Blazor, and applications that depend heavily on dynamic runtime features may not be suitable for the same Native AOT publishing workflow. Verify support for your application's specific framework features before starting the migration.
Step 1: Create an ASP.NET Core Minimal API
Start with a supported .NET SDK and create a minimal web application.
dotnet new web -n NativeAotApi
cd NativeAotApi
This creates a basic ASP.NET Core project.
Open Program.cs and replace its contents with the following implementation:
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
app.MapGet("/", () => Results.Ok(new
{
Service = "NativeAotApi",
Status = "Running"
}));
app.MapGet("/health", () => Results.Ok(new
{
Status = "Healthy"
}));
app.MapGet("/api/products/{id:int}", (int id) =>
{
if (id <= 0)
{
return Results.BadRequest(new
{
Error = "Product ID must be greater than zero."
});
}
return Results.Ok(new
{
Id = id,
Name = $"Product {id}",
Price = 499.00m
});
});
app.Run();
The application exposes three endpoints:
/ returns basic service information.
/health provides a simple health-check response.
/api/products/{id} returns a sample product for a valid positive identifier.
The example deliberately avoids a database, external services, and runtime plugin loading. This keeps the initial Native AOT implementation straightforward.
WebApplication.CreateSlimBuilder is designed for applications that benefit from a smaller hosting configuration. It is useful for Native AOT scenarios, but it is not required for every ASP.NET Core application that uses AOT.
Step 2: Enable Native AOT in the Project
Open NativeAotApi.csproj and configure the project for Native AOT publishing.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
This example targets .NET 10. If your project targets another supported .NET release, use the corresponding target framework and verify its Native AOT support.
The important setting is:
<PublishAot>true</PublishAot>
It instructs the SDK to publish the application using Native AOT.
The OutputType property identifies the project as an executable application. The SDK can often infer this for web projects, but declaring it makes the intended output explicit.
InvariantGlobalization is optional. It reduces globalization dependencies by using invariant culture behavior rather than full culture-specific data. Do not enable it blindly if your service formats dates, numbers, currencies, or text according to specific cultures.
Native AOT publishing also requires an appropriate native toolchain for the target platform. Depending on the operating system, this can include a C/C++ compiler, linker, and other build dependencies.
Step 3: Publish for the Target Platform
Native AOT produces a platform-specific executable. A binary built for Linux x64 is not interchangeable with one built for Windows x64 or Linux ARM64.
Publish for the environment where the service will run.
For Linux x64:
dotnet publish -c Release -r linux-x64
For Windows x64:
dotnet publish -c Release -r win-x64
For Linux ARM64:
dotnet publish -c Release -r linux-arm64
The runtime identifier (-r) selects the target operating system and architecture.
For Native AOT, the SDK invokes the required compilation steps and produces a native executable. The publish process may take longer than a normal build because it performs additional analysis and native compilation.
Inspect the publish output directory to locate the executable and any additional files required by the application.
For a typical Linux x64 project, the output is under a path similar to:
bin/Release/net10.0/linux-x64/publish/
The exact files depend on the SDK, project configuration, and target platform.
Step 4: Run and Test the Native Executable
After publishing, execute the generated binary directly rather than running the application through dotnet run.
On Linux:
./bin/Release/net10.0/linux-x64/publish/NativeAotApi
On Windows, run the generated .exe from the publish directory.
Once the service starts, call the health endpoint:
curl http://localhost:5000/health
The port depends on the application's hosting configuration and environment. Check the startup output or configure the intended listening address explicitly if necessary.
A successful response should contain a JSON object similar to:
{
"status": "Healthy"
}
Also test the product endpoint:
curl http://localhost:5000/api/products/10
The service should return the sample product data.
These checks confirm that the published executable starts and responds. They do not establish that every dependency, production configuration, or operational requirement is working correctly.
Before deployment, test the service using the same environment variables, certificates, network settings, and permissions that it will use in production.
Step 5: Handle JSON Serialization Carefully
JSON serialization is a common source of Native AOT compatibility issues.
Some serialization patterns rely on runtime reflection to discover object properties and generate serialization metadata. Such patterns may trigger trimming warnings or require additional configuration when publishing with Native AOT.
For predictable results, use System.Text.Json source generation where appropriate.
For example, define a response model and a JSON serialization context:
using System.Text.Json.Serialization;
public sealed record ProductResponse(
int Id,
string Name,
decimal Price);
[JsonSerializable(typeof(ProductResponse))]
internal partial class AppJsonSerializerContext
: JsonSerializerContext
{
}
The source generator creates serialization metadata at compile time.
You can then use the generated context when serializing the response directly:
using System.Text.Json;
var product = new ProductResponse(10, "Keyboard", 499.00m);
string json = JsonSerializer.Serialize(
product,
AppJsonSerializerContext.Default.ProductResponse);
Console.WriteLine(json);
In a complete ASP.NET Core application, register the generated serialization metadata with the appropriate HTTP JSON options when those models are used by Minimal API endpoints. The exact registration should match the application's endpoint and serialization configuration.
Source generation is especially useful when the application has known request and response types. It makes the required serialization metadata explicit and reduces dependence on runtime reflection.
Step 6: Understand Trimming and Compatibility Warnings
Native AOT publishing includes analysis that identifies code which may be incompatible with trimming or ahead-of-time compilation.
A warning may indicate that a library uses reflection, dynamically loads types, or invokes functionality that cannot be determined reliably during compilation.
Do not treat these warnings as harmless build noise.
For each warning:
Identify the package or method that generates it.
Determine whether the affected code path is used by the application.
Check whether the library supports Native AOT.
Prefer source-generated or statically registered alternatives when available.
Run tests against the published executable, not only against the development build.
Some warnings require application changes, while others may be addressed through updated packages or supported annotations.
Suppressing warnings without understanding their implications can lead to runtime failures when trimmed code or metadata is needed but has been removed.
Step 7: Benchmark Before Adopting Native AOT
Native AOT is a deployment and compilation strategy, not a guarantee that every request will execute faster.
Measure the application in at least four areas:
Startup time: How long the process takes to become ready.
Memory consumption: The baseline and peak memory required under load.
Throughput: The number of requests the service can process per unit of time.
Latency: Request response times under realistic traffic.
Compare Native AOT against a conventional deployment using the same application logic, hardware, operating system, and workload.
A small service that starts frequently may benefit more from reduced startup overhead than a long-running service whose performance is dominated by database calls.
Likewise, a service that spends most of its time waiting on remote dependencies may see limited improvements in request latency.
Native AOT can also increase build time, so include CI duration and artifact size in the evaluation.
Step 8: Deploy the Service with Production Controls
Before releasing the native executable, validate its behavior in the intended deployment environment.
Check the following:
Configuration and secrets are provided through the expected deployment mechanism.
Health checks correctly identify when the service is ready.
Logging and metrics work with the production observability stack.
TLS certificates and outbound network connections are configured correctly.
The executable has the correct file permissions.
The container or host provides required native libraries.
The service shuts down cleanly when the platform sends a termination signal.
The deployment pipeline rebuilds the application when its toolchain or dependencies change.
For containerized deployments, build for the target operating system and architecture. A native executable does not eliminate every operating-system dependency, and the final container image must still provide the required native libraries.
Pin and update the build environment through the normal software supply-chain process. Native AOT applications still need security patches, dependency updates, and regular rebuilding.
Common Mistakes to Avoid
Assuming Native AOT Supports Every ASP.NET Core Feature
Some framework features depend on runtime behavior that does not fit the Native AOT model. Verify support for the application's specific hosting model and dependencies before committing to the migration.
Ignoring Reflection-Related Warnings
Warnings can reveal missing metadata or code paths that may fail after trimming. Investigate them instead of suppressing them automatically.
Publishing for the Wrong Architecture
A native executable must match the target platform. Confirm the runtime identifier used in the publishing pipeline and test the actual artifact that will be deployed.
Measuring Only Startup Time
Faster startup does not automatically mean better steady-state throughput or lower request latency. Benchmark the full workload and monitor memory usage under load.
Assuming the Executable Has No External Dependencies
Native AOT reduces reliance on the .NET runtime installation, but operating-system libraries and other native dependencies may still be required. Test the published binary in a clean target environment.
Summary
Native AOT provides an alternative deployment model for ASP.NET Core services by compiling application code into native machine code during publishing. For suitable Minimal API applications, it can reduce startup overhead, lower memory consumption in some workloads, and simplify runtime deployment.
The process begins by selecting a compatible application model, enabling PublishAot, publishing for the target platform, and testing the resulting executable. Developers should pay particular attention to JSON serialization, reflection-related warnings, native dependencies, and library compatibility.
The best candidates are services with predictable dependencies and limited reliance on dynamic runtime behavior. Before adopting Native AOT in production, compare it with a conventional deployment using real workload measurements and validate the complete deployment process.
Join the conversation! Your thoughts help the community grow.