What is Native AOT in .NET?
When you compile your C# code, it gets converted into Intermediate Language (IL). Then, the .NET Runtime (CLR) uses the Just-In-Time (JIT) compiler to turn that IL into machine code during execution.
But here’s where things get interesting: Native AOT skips that intermediate step. Instead, it compiles your C# code directly into native machine code on your machine. For example, on Windows, you get an executable (.exe) file directly.
Although the process still technically involves IL, it’s done transparently within a single compilation step, effectively serving you a ready-to-go native executable.
Advantages and Disadvantages of Native AOT in .NET
Like any powerful tool, Native AOT comes with its own set of pros and cons. Understanding these will help you decide if it’s the right fit for your project.
Benefits
- Performance Gains: Compiling directly to machine code eliminates the JIT compilation step during runtime, which means faster execution, especially on the first run.
- Reduced Startup Time: Ideal for applications like Azure Functions or AWS Lambda that benefit from quicker startup times.
- Self-Contained Executables: The resulting executable includes everything needed to run your app, so the target machine doesn’t need the .NET Runtime installed.
Drawbacks
- Platform Specificity: Native AOT compels you to compile the application for each target OS. A Windows-compiled .exe won’t run on Linux.
- Larger File Sizes: Both the compilation time and resulting application size tend to be larger compared to traditional compilation.
- Compatibility Issues: Not all libraries and functionalities are compatible with Native AOT. For instance, Entity Framework Core and certain WebAPI features aren’t supported yet.
A Practical Example: Native AOT in Action
Now that we’ve covered the theory, let’s put it into practice by comparing a traditional .NET app with one compiled using Native AOT. We’ll be using a minimal API supported by Native AOT to demonstrate the process.
Preparing Your Environment
Before you begin, ensure your development environment is set up correctly. You’ll need to install the Desktop development workload with C++ using the Visual Studio Installer.
Setting Up a Minimal API with Native AOT
Let’s walk through the steps to create a minimal API and compile it using Native AOT. We’ll explore the differences from a traditional setup along the way.
Step 1: Create a New Project
Open Visual Studio and create a new project. You can search for “ASP.NET Core Empty” to start with a minimal API template.
Step 2: Configure with
In your Program.cs
file, replace the standard setup with CreateSlimBuilder
to create a minimal web application.
// Create a SlimBuilder instance to set up a minimal web application
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
// Define a route that responds with "Hello World!" for requests to the root URL
app.MapGet("/", () => "Hello World!");
// Start the application
app.Run();
Step 3: Enable Native AOT in Project File
Modify your project file (.csproj) by adding the PublishAot
property. This will instruct the compiler to use Native AOT.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>
By setting PublishAot
to true
, you’re enabling the Native AOT feature, which will generate a self-contained executable.
Comparing File Sizes
Now let’s publish the project to see the differences in output file sizes.
Traditional .NET Output
For a traditional .NET publish, you would typically see a .dll
file along with an executable loader.
dotnet publish -c Release
Native AOT Output
With Native AOT, the output is a single executable file that contains everything needed to run your application.
dotnet publish -c Release -r win-x64
In the publish directory, you will find:
- Traditional .NET Output: A
.dll
file and an executable (.exe
). - Native AOT Output: One sizable
.exe
file without additional dependencies.
Measuring Performance
Let’s delve into performance to see if Native AOT delivers on its promise. We’ll add middleware to measure execution time and perform a simple database query using Dapper, which is compatible with Native AOT.
Adding Middleware for Execution Time
First, we’ll add middleware to log execution times.
// Adding middleware to measure execution time
app.Use(async (context, next) => {
var watch = System.Diagnostics.Stopwatch.StartNew();
await next();
watch.Stop();
var executionTime = watch.ElapsedMilliseconds;
Console.WriteLine($"Execution Time: {executionTime} ms");
});
Performing a Database Query with Dapper
Next, we’ll add a simple database query using Dapper.
// Simple database query with Dapper
app.MapGet("/data", async () => {
using var connection = new SqlConnection("your_connection_string");
var data = await connection.QueryAsync("SELECT * FROM YourTable");
return data;
});
After implementing these changes, publish the project using both traditional and Native AOT methods and run several tests.
Real-Life Examples
Let’s consider a real-life scenario where Native AOT can be beneficial. Imagine an Azure Function that needs to start quickly to handle HTTP requests.
Azure Function Example
Here’s a simplified example of an Azure Function using Native AOT.
public static class Function1 {
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log) {
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
By compiling this Azure Function with Native AOT, you can significantly reduce the cold start time, improving performance and reducing costs.
Conclusion
Native AOT in .NET can offer substantial performance improvements, especially for applications where startup time is critical. While there are some limitations and additional considerations, the benefits can be compelling.
-
Benefits:
- Improved performance through reduced startup times
- Self-contained executables eliminating dependency issues on target environments
-
Considerations:
- Platform specificity requiring multiple compilations for different OSes
- Potential increase in file sizes and longer compilation times
- Compatibility limitations with existing libraries and frameworks
As .NET continues to evolve, we can expect more libraries and frameworks to support Native AOT, making it an even more attractive option for developers looking to optimize their applications. So why not give it a try and see how much you can boost your app’s performance?
Top comments (4)
Nice. But, I get 10 files in the output folder. Can I package it into 1 file?
Hey Mark, sure!
will it reduce api response time too, or it just helps in start time?
It mostly just helps with startup time. There will be a small gain in general performance but the biggest win is a smaller footprint for executables and a significantly shorter startup time.