DEV Community

Cover image for The Ultimate Guide to .NET Native AOT: Benefits and Examples 🤫
ByteHide
ByteHide

Posted on

The Ultimate Guide to .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();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");

});
Enter fullscreen mode Exit fullscreen mode

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;

});
Enter fullscreen mode Exit fullscreen mode

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");

    }

}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
markpelf profile image
Mark Pelf

Nice. But, I get 10 files in the output folder. Can I package it into 1 file?

Collapse
 
bytehide profile image
ByteHide

Hey Mark, sure!

Collapse
 
engineer profile image
Rushikesh Suradkar

will it reduce api response time too, or it just helps in start time?

Collapse
 
caybo_kotze profile image
Caybo Kotzé

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.