Mastering .NET Native AOT: Benefits and Examples 🚹

Juan España
ByteHide
Published in
5 min readMay 30, 2024

--

In this article, you’ll dive into Native AOT (Ahead-Of-Time) Compilation in .NET. This innovative feature has been the centerpiece of several .NET versions, enhancing performances. We’ll explore how Native AOT works, its benefits and drawbacks, and show you a practical example to illustrate its potential.

Mastering .NET Native AOT: Benefits and Examples

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.

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?

--

--

Juan España
ByteHide
Editor for

CEO at ByteHide🔐, passionate about highly scalable technology businesses and .NET content creator đŸ‘šâ€đŸ’»