.NET Minimal APIs and Native AOT


.NET Minimal APIs revolutionized API development by reducing boilerplate code, while Native AOT (Ahead-of-Time compilation) delivers lightning-fast startup times and reduced memory footprint. This guide covers everything you need to master both technologies.

What are Minimal APIs?

Minimal APIs were introduced in .NET 6 as a simplified way to build HTTP APIs without the ceremony of controllers, action methods, and heavy abstractions.

Traditional Controller-Based API

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task<ActionResult<List<Product>>> GetAll()
    {
        var products = await _productService.GetAllAsync();
        return Ok(products);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        if (product == null)
            return NotFound();
        return Ok(product);
    }
}

Minimal API Equivalent

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/api/products", async (IProductService productService) =>
{
    var products = await productService.GetAllAsync();
    return Results.Ok(products);
});

app.MapGet("/api/products/{id}", async (int id, IProductService productService) =>
{
    var product = await productService.GetByIdAsync(id);
    return product is not null ? Results.Ok(product) : Results.NotFound();
});

app.Run();

Key Benefits:

  • Less code: ~60% fewer lines compared to controllers
  • Faster development: No need for controller classes, inheritance
  • Performance: Slightly better throughput due to less abstraction
  • Modern syntax: Uses latest C# features (pattern matching, top-level statements)

When to Use Minimal APIs

✅ Use Minimal APIs When:

  • Building microservices with simple CRUD operations
  • Creating lightweight APIs for serverless/containers
  • Prototyping or building MVPs quickly
  • You prefer functional programming style
  • Performance is critical (native AOT compatibility)
  • Building simple webhooks or event handlers

⚠️ Consider Controllers When:

  • Building large enterprise applications with hundreds of endpoints
  • Team prefers object-oriented approach
  • You need extensive controller features (filters, model binding customization)
  • Existing codebase uses controllers heavily

Creating Your First Minimal API

1. Create New Project

dotnet new web -n MinimalApiDemo
cd MinimalApiDemo

2. Basic Program.cs

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

3. Run the Application

dotnet run
# Navigate to http://localhost:5000

HTTP Methods and Routing

All HTTP Verbs

// GET
app.MapGet("/products", () => /* ... */);

// POST
app.MapPost("/products", (Product product) => /* ... */);

// PUT
app.MapPut("/products/{id}", (int id, Product product) => /* ... */);

// DELETE
app.MapDelete("/products/{id}", (int id) => /* ... */);

// PATCH
app.MapPatch("/products/{id}", (int id, JsonPatchDocument patch) => /* ... */);

Route Parameters

// Simple parameter
app.MapGet("/products/{id}", (int id) =>
    Results.Ok($"Product ID: {id}"));

// Multiple parameters
app.MapGet("/categories/{categoryId}/products/{productId}",
    (int categoryId, int productId) =>
        Results.Ok(new { categoryId, productId }));

// Optional parameters (use nullable types)
app.MapGet("/search", (string? query, int? page) =>
    Results.Ok($"Query: {query ?? "all"}, Page: {page ?? 1}"));

// Route constraints
app.MapGet("/products/{id:int}", (int id) => /* ... */);
app.MapGet("/products/{slug:alpha}", (string slug) => /* ... */);
app.MapGet("/items/{id:guid}", (Guid id) => /* ... */);

Query String Parameters

// Automatic binding from query string
app.MapGet("/products", (int page, int pageSize, string? search) =>
{
    // GET /products?page=1&pageSize=10&search=laptop
    return Results.Ok(new
    {
        Page = page,
        PageSize = pageSize,
        Search = search ?? ""
    });
});

Request and Response Handling

Reading Request Body

// Simple object
app.MapPost("/products", (Product product) =>
{
    // Product automatically deserialized from JSON
    return Results.Created($"/products/{product.Id}", product);
});

// With validation
app.MapPost("/products", (Product product) =>
{
    if (string.IsNullOrEmpty(product.Name))
        return Results.BadRequest("Name is required");

    return Results.Created($"/products/{product.Id}", product);
});

// Manual HttpContext access
app.MapPost("/upload", async (HttpContext context) =>
{
    using var reader = new StreamReader(context.Request.Body);
    var body = await reader.ReadToEndAsync();
    return Results.Ok($"Received: {body}");
});

Response Types

// Ok (200)
app.MapGet("/products/{id}", (int id) =>
    Results.Ok(new Product { Id = id, Name = "Laptop" }));

// Created (201)
app.MapPost("/products", (Product product) =>
    Results.Created($"/products/{product.Id}", product));

// NoContent (204)
app.MapDelete("/products/{id}", (int id) =>
    Results.NoContent());

// NotFound (404)
app.MapGet("/products/{id}", (int id) =>
    Results.NotFound());

// BadRequest (400)
app.MapPost("/products", (Product product) =>
    Results.BadRequest("Invalid product data"));

// Custom status code
app.MapGet("/health", () =>
    Results.StatusCode(503)); // Service Unavailable

// File download
app.MapGet("/download", () =>
    Results.File("report.pdf", "application/pdf"));

// Redirect
app.MapGet("/old-path", () =>
    Results.Redirect("/new-path"));

Dependency Injection

Registering Services

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddSingleton<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddTransient<IEmailService, EmailService>();

// Add database context
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

Injecting into Endpoints

// Service injection (automatically resolved)
app.MapGet("/products", async (IProductService productService) =>
{
    var products = await productService.GetAllAsync();
    return Results.Ok(products);
});

// Multiple services
app.MapPost("/products", async (
    Product product,
    IProductService productService,
    ILogger<Program> logger) =>
{
    logger.LogInformation("Creating product: {Name}", product.Name);
    await productService.CreateAsync(product);
    return Results.Created($"/products/{product.Id}", product);
});

// Built-in services
app.MapGet("/config", (IConfiguration config) =>
    Results.Ok(config["AppSettings:Version"]));

Organizing Minimal APIs

Route Groups

var app = builder.Build();

// Product routes
var products = app.MapGroup("/api/products")
    .WithTags("Products");

products.MapGet("/", GetAllProducts);
products.MapGet("/{id}", GetProductById);
products.MapPost("/", CreateProduct);
products.MapPut("/{id}", UpdateProduct);
products.MapDelete("/{id}", DeleteProduct);

// Category routes with prefix
var categories = app.MapGroup("/api/categories")
    .WithTags("Categories")
    .RequireAuthorization(); // All category routes require auth

categories.MapGet("/", GetAllCategories);
categories.MapGet("/{id}", GetCategoryById);

app.Run();

Separate Endpoint Classes (Carter Pattern)

// ProductEndpoints.cs
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products")
            .WithOpenApi();

        group.MapGet("/", GetAll);
        group.MapGet("/{id}", GetById);
        group.MapPost("/", Create);
        group.MapPut("/{id}", Update);
        group.MapDelete("/{id}", Delete);
    }

    private static async Task<IResult> GetAll(IProductService service)
    {
        var products = await service.GetAllAsync();
        return Results.Ok(products);
    }

    private static async Task<IResult> GetById(int id, IProductService service)
    {
        var product = await service.GetByIdAsync(id);
        return product is not null ? Results.Ok(product) : Results.NotFound();
    }

    private static async Task<IResult> Create(Product product, IProductService service)
    {
        var created = await service.CreateAsync(product);
        return Results.Created($"/api/products/{created.Id}", created);
    }

    private static async Task<IResult> Update(
        int id, Product product, IProductService service)
    {
        await service.UpdateAsync(id, product);
        return Results.NoContent();
    }

    private static async Task<IResult> Delete(int id, IProductService service)
    {
        await service.DeleteAsync(id);
        return Results.NoContent();
    }
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapProductEndpoints();

app.Run();

Validation and Error Handling

Manual Validation

app.MapPost("/products", (Product product) =>
{
    if (string.IsNullOrWhiteSpace(product.Name))
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            { "Name", new[] { "Name is required" } }
        });

    if (product.Price <= 0)
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            { "Price", new[] { "Price must be greater than 0" } }
        });

    return Results.Created($"/products/{product.Id}", product);
});

FluentValidation Integration

dotnet add package FluentValidation.DependencyInjectionExtensions
// ProductValidator.cs
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100);

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be positive");

        RuleFor(x => x.Stock)
            .GreaterThanOrEqualTo(0).WithMessage("Stock cannot be negative");
    }
}

// Program.cs
builder.Services.AddValidatorsFromAssemblyContaining<Program>();

app.MapPost("/products", async (
    Product product,
    IValidator<Product> validator,
    IProductService service) =>
{
    var validationResult = await validator.ValidateAsync(product);
    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(
            validationResult.ToDictionary());
    }

    var created = await service.CreateAsync(product);
    return Results.Created($"/products/{created.Id}", created);
});

Global Exception Handler

app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var exceptionHandlerFeature =
            context.Features.Get<IExceptionHandlerFeature>();

        var error = new
        {
            message = "An error occurred",
            detail = exceptionHandlerFeature?.Error.Message
        };

        await context.Response.WriteAsJsonAsync(error);
    });
});

Authentication and Authorization

JWT Authentication

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Public endpoint
app.MapGet("/public", () => "Public data");

// Protected endpoint
app.MapGet("/private", () => "Private data")
    .RequireAuthorization();

// Role-based authorization
app.MapGet("/admin", () => "Admin data")
    .RequireAuthorization("Admin");

// Policy-based authorization
app.MapGet("/manager", () => "Manager data")
    .RequireAuthorization(policy => policy.RequireRole("Manager", "Admin"));

Native AOT: What and Why

What is Native AOT?

Native AOT (Ahead-of-Time compilation) compiles your .NET application directly to native machine code before runtime, instead of using Just-In-Time (JIT) compilation.

Traditional .NET (JIT):

C# Source → IL Code → (Runtime) JIT → Native Code

Native AOT:

C# Source → Native Code (at build time)

Why Use Native AOT?

Benefits:

  1. Fast Startup: 4-10x faster startup times

    • Traditional: 500ms-2s
    • Native AOT: 50-200ms
  2. Lower Memory Usage: 50-70% reduction

    • Traditional: 50-100MB baseline
    • Native AOT: 10-30MB baseline
  3. Smaller Deployment: Single executable, no runtime needed

    • Traditional: 100-200MB
    • Native AOT: 10-50MB
  4. Predictable Performance: No JIT warmup time

Trade-offs:

  1. Longer Build Times: 2-10x slower builds
  2. Limited Reflection: Only source-generated or trimming-safe
  3. No Dynamic Code: No Assembly.Load, Reflection.Emit
  4. Library Compatibility: Not all NuGet packages support AOT

When to Use AOT

✅ Perfect for:

  • Serverless functions (AWS Lambda, Azure Functions)
  • Microservices with frequent cold starts
  • CLI tools and utilities
  • Edge computing / IoT devices
  • Container-based applications (smaller images)

❌ Avoid for:

  • Applications using heavy reflection
  • Apps with dynamic plugin systems
  • Complex Entity Framework scenarios
  • Third-party libraries without AOT support

Enabling Native AOT

1. Create AOT-Compatible Project

dotnet new webapiaot -n MyApiAot
cd MyApiAot

Or modify existing .csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    
    <!-- Enable Native AOT -->
    <PublishAot>true</PublishAot>
    
    <!-- Optional: Reduce size further -->
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>

2. Write AOT-Compatible Code

using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

// Configure JSON serialization for AOT
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapGet("/products", () =>
    new Product { Id = 1, Name = "Laptop", Price = 999.99m });

app.Run();

// Required for AOT - JSON source generation
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

public record Product(int Id, string Name, decimal Price);

3. Publish Native Executable

# Publish for current OS
dotnet publish -c Release

# Publish for specific runtime
dotnet publish -c Release -r linux-x64
dotnet publish -c Release -r win-x64
dotnet publish -c Release -r osx-arm64

Output: Single native executable (10-50MB) with no dependencies.

AOT-Compatible Patterns

Using SlimBuilder

// Standard builder (includes many services)
var builder = WebApplication.CreateBuilder(args);

// Slim builder (minimal services for AOT)
var builder = WebApplication.CreateSlimBuilder(args);

// Manually add only what you need
builder.Services.AddSingleton<IProductService, ProductService>();

JSON Source Generation

// Define all types that will be serialized
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSerializable(typeof(ApiResponse<Product>))]
[JsonSerializable(typeof(ErrorResponse))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

// Configure in builder
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0,
        AppJsonSerializerContext.Default);
});

Avoiding Reflection

// ❌ Not AOT-compatible (reflection)
var type = Type.GetType("MyNamespace.Product");
var instance = Activator.CreateInstance(type);

// ✅ AOT-compatible (direct instantiation)
var product = new Product { Id = 1, Name = "Laptop" };

// ❌ Not AOT-compatible (dynamic loading)
var assembly = Assembly.Load("MyPlugin.dll");

// ✅ AOT-compatible (compile-time known)
var service = new ProductService();

Complete AOT Example

using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

// Add logging
builder.Logging.AddConsole();

// Configure JSON with source generation
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0,
        AppJsonSerializerContext.Default);
});

// Register services
builder.Services.AddSingleton<IProductRepository, InMemoryProductRepository>();

var app = builder.Build();

// Endpoints
var products = app.MapGroup("/api/products");

products.MapGet("/", (IProductRepository repo) =>
{
    var allProducts = repo.GetAll();
    return Results.Ok(allProducts);
});

products.MapGet("/{id}", (int id, IProductRepository repo) =>
{
    var product = repo.GetById(id);
    return product is not null ? Results.Ok(product) : Results.NotFound();
});

products.MapPost("/", (Product product, IProductRepository repo) =>
{
    var created = repo.Add(product);
    return Results.Created($"/api/products/{created.Id}", created);
});

products.MapDelete("/{id}", (int id, IProductRepository repo) =>
{
    repo.Delete(id);
    return Results.NoContent();
});

app.Run();

// Models with JSON source generation
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

public record Product(int Id, string Name, decimal Price, int Stock);

// Repository
public interface IProductRepository
{
    List<Product> GetAll();
    Product? GetById(int id);
    Product Add(Product product);
    void Delete(int id);
}

public class InMemoryProductRepository : IProductRepository
{
    private readonly List<Product> _products = new()
    {
        new(1, "Laptop", 999.99m, 10),
        new(2, "Mouse", 29.99m, 50),
        new(3, "Keyboard", 79.99m, 25)
    };

    public List<Product> GetAll() => _products;

    public Product? GetById(int id) => _products.FirstOrDefault(p => p.Id == id);

    public Product Add(Product product)
    {
        var newProduct = product with { Id = _products.Max(p => p.Id) + 1 };
        _products.Add(newProduct);
        return newProduct;
    }

    public void Delete(int id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product is not null)
            _products.Remove(product);
    }
}

Performance Comparison

Startup Time Benchmark

Application: Simple API with 10 endpoints

Traditional .NET 8 (JIT):
- Cold start: 892ms
- Memory: 62MB

Native AOT:
- Cold start: 128ms (7x faster)
- Memory: 18MB (3.4x less)

Memory Footprint

Application: REST API with database

Traditional:
- Baseline: 75MB
- Under load: 150MB
- Peak: 220MB

Native AOT:
- Baseline: 22MB (71% reduction)
- Under load: 45MB (70% reduction)
- Peak: 68MB (69% reduction)

Deployment Size

Traditional publish:
- Runtime + dependencies: 180MB
- Application: 25MB
- Total: 205MB

Native AOT publish:
- Single executable: 32MB (84% reduction)
- No runtime needed

Best Practices

1. Use CreateSlimBuilder for AOT

// For AOT
var builder = WebApplication.CreateSlimBuilder(args);

// For traditional apps
var builder = WebApplication.CreateBuilder(args);

2. Always Use JSON Source Generation

[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

3. Test AOT Compatibility Early

# Check for AOT warnings
dotnet publish -c Release /p:PublishAot=true

4. Keep Dependencies Minimal

// Only add packages that support AOT
// Check package documentation

5. Use Records for DTOs

// Record types work great with source generators
public record ProductDto(int Id, string Name, decimal Price);

6. Avoid Reflection

// Use dependency injection instead of reflection
builder.Services.AddSingleton<IProductService, ProductService>();

Common Pitfalls and Solutions

Issue: JsonSerializer throws at runtime

Problem:

// Missing source generation
return Results.Ok(new Product { ... });
// Runtime error: Type 'Product' not supported

Solution:

[JsonSerializable(typeof(Product))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

Issue: EF Core queries fail

Problem:

// Complex LINQ with reflection
context.Products.Where(p => p.Name.Contains(searchTerm))

Solution: Use compiled queries or simpler LINQ with source generation support.

Issue: Slow build times

Problem: Full AOT publish takes 5+ minutes

Solution:

# During development, use JIT
dotnet run

# Only use AOT for production builds
dotnet publish -c Release -p:PublishAot=true

Minimal APIs + AOT: Production Example

Here’s a production-ready example combining everything:

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateSlimBuilder(args);

// Logging
builder.Logging.AddJsonConsole();

// JSON configuration
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0,
        AppJsonSerializerContext.Default);
});

// Services
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddSingleton<IMetricsCollector, MetricsCollector>();

// Health checks
builder.Services.AddHealthChecks();

var app = builder.Build();

// Middleware
app.UseHealthChecks("/health");

// API endpoints
var api = app.MapGroup("/api/v1");

api.MapGet("/products", GetProducts)
    .WithName("GetProducts")
    .Produces<List<Product>>();

api.MapGet("/products/{id:int}", GetProduct)
    .WithName("GetProduct")
    .Produces<Product>()
    .Produces(404);

api.MapPost("/products", CreateProduct)
    .WithName("CreateProduct")
    .Produces<Product>(201)
    .Produces(400);

app.Run();

// Handlers
static async Task<Ok<List<Product>>> GetProducts(
    IProductService service,
    ILogger<Program> logger)
{
    logger.LogInformation("Fetching all products");
    var products = await service.GetAllAsync();
    return TypedResults.Ok(products);
}

static async Task<Results<Ok<Product>, NotFound>> GetProduct(
    int id,
    IProductService service)
{
    var product = await service.GetByIdAsync(id);
    return product is not null
        ? TypedResults.Ok(product)
        : TypedResults.NotFound();
}

static async Task<Results<Created<Product>, ValidationProblem>> CreateProduct(
    Product product,
    IProductService service)
{
    if (string.IsNullOrWhiteSpace(product.Name))
        return TypedResults.ValidationProblem(new Dictionary<string, string[]>
        {
            ["Name"] = new[] { "Name is required" }
        });

    var created = await service.CreateAsync(product);
    return TypedResults.Created($"/api/v1/products/{created.Id}", created);
}

// JSON Source Generation
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSerializable(typeof(Dictionary<string, string[]>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

// Models
public record Product(int Id, string Name, decimal Price, int Stock);

// Services
public interface IProductService
{
    Task<List<Product>> GetAllAsync();
    Task<Product?> GetByIdAsync(int id);
    Task<Product> CreateAsync(Product product);
}

public class ProductService : IProductService
{
    private readonly List<Product> _products = new()
    {
        new(1, "Laptop", 999.99m, 10),
        new(2, "Mouse", 29.99m, 50)
    };

    public Task<List<Product>> GetAllAsync() =>
        Task.FromResult(_products);

    public Task<Product?> GetByIdAsync(int id) =>
        Task.FromResult(_products.FirstOrDefault(p => p.Id == id));

    public Task<Product> CreateAsync(Product product)
    {
        var newProduct = product with { Id = _products.Max(p => p.Id) + 1 };
        _products.Add(newProduct);
        return Task.FromResult(newProduct);
    }
}

public interface IMetricsCollector
{
    void RecordRequest(string endpoint);
}

public class MetricsCollector : IMetricsCollector
{
    public void RecordRequest(string endpoint) { }
}

Docker Integration

Dockerfile for AOT

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY ["MyApi.csproj", "./"]
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -o /app/publish -r linux-x64 -p:PublishAot=true

# Use minimal runtime image
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine
WORKDIR /app
COPY --from=build /app/publish .

# Create non-root user
RUN adduser -D -u 1000 appuser && \
    chown -R appuser:appuser /app
USER appuser

EXPOSE 8080
ENTRYPOINT ["./MyApi"]

Result:

  • Traditional image: ~200MB
  • AOT image: ~35MB (5.7x smaller)

References

See also