.NET 8 and C# 12: A Deep Dive into Native AOT, Primary Constructors, and Blazor United

Introduction: .NET 8 represents a landmark release in Microsoft’s development platform evolution, bringing Native AOT to mainstream scenarios, unifying Blazor’s rendering models, and introducing C# 12’s powerful new features. Released in November 2023, this Long-Term Support version delivers significant performance improvements, reduced memory footprint, and enhanced developer productivity. After migrating several enterprise applications to .NET 8, I’ve found the combination of Native AOT compilation and C# 12’s primary constructors dramatically simplifies code while improving runtime characteristics. Organizations should prioritize .NET 8 adoption for new projects and plan migrations for existing applications to leverage these substantial improvements.

Native AOT: Ahead-of-Time Compilation for Production

Native AOT (Ahead-of-Time) compilation transforms .NET applications into self-contained native executables without requiring the .NET runtime. This compilation model eliminates JIT compilation overhead, reduces application startup time to milliseconds, and produces smaller deployment artifacts. For containerized microservices and serverless functions, Native AOT delivers dramatic improvements in cold start performance.

.NET 8 expands Native AOT support to ASP.NET Core web APIs, enabling high-performance REST services with minimal footprint. A typical minimal API compiled with Native AOT produces executables under 10MB, starts in under 50ms, and consumes significantly less memory than JIT-compiled equivalents. These characteristics make Native AOT ideal for Kubernetes deployments where rapid scaling and resource efficiency matter.

Trimming improvements in .NET 8 enhance Native AOT compatibility. The trimmer now handles more reflection patterns, reducing the need for manual annotations. Source generators replace runtime reflection for JSON serialization, dependency injection, and configuration binding, ensuring trim-safe code that works seamlessly with Native AOT.

C# 12 Language Features

Primary constructors extend beyond records to all classes and structs, reducing boilerplate for dependency injection and initialization patterns. Constructor parameters become available throughout the class body, eliminating the need for explicit field declarations and assignments. This feature particularly benefits ASP.NET Core controllers and services where constructor injection is ubiquitous.

Collection expressions provide concise syntax for creating arrays, lists, and spans. The spread operator enables combining collections inline, while target-typed new expressions infer collection types from context. These features make collection initialization more readable and reduce ceremony in data transformation code.

Default lambda parameters enable optional arguments in lambda expressions, improving API design for callback-heavy code. Alias any type extends using directives to support tuples, arrays, and pointer types, enabling domain-specific type aliases that improve code clarity.

Blazor United: Full-Stack Web Development

Blazor in .NET 8 unifies server-side and WebAssembly rendering models into a cohesive full-stack framework. Components can now specify their render mode declaratively, enabling per-component decisions about server streaming, WebAssembly interactivity, or static server rendering. This flexibility allows optimizing each component for its specific requirements.

Streaming rendering improves perceived performance by sending initial HTML immediately while async operations complete. Users see content progressively rather than waiting for all data to load. Combined with enhanced navigation and form handling, Blazor delivers SPA-like experiences with server-side rendering benefits.

C# Implementation: Modern .NET 8 Patterns

Here’s a comprehensive implementation demonstrating .NET 8 and C# 12 features:

// .NET 8 and C# 12 Modern Patterns
// Demonstrates primary constructors, collection expressions, Native AOT compatibility

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// ==================== Type Aliases (C# 12) ====================

using ProductId = System.Guid;
using CustomerId = System.Guid;
using OrderItems = System.Collections.Generic.List<OrderItem>;
using PriceMap = System.Collections.Generic.Dictionary<string, decimal>;

// ==================== Primary Constructors (C# 12) ====================

/// <summary>
/// Product entity using primary constructor.
/// Constructor parameters are available throughout the class.
/// </summary>
public class Product(
    ProductId id,
    string name,
    decimal price,
    string category,
    int stockQuantity = 0)
{
    public ProductId Id { get; } = id;
    public string Name { get; } = name;
    public decimal Price { get; } = price;
    public string Category { get; } = category;
    public int StockQuantity { get; set; } = stockQuantity;
    
    // Primary constructor parameter used in method
    public bool IsInStock() => stockQuantity > 0;
    
    public decimal CalculateDiscount(decimal percentage) => 
        price * (percentage / 100m);
}

/// <summary>
/// Service with primary constructor for dependency injection.
/// Eliminates boilerplate field declarations and assignments.
/// </summary>
public class ProductService(
    IProductRepository repository,
    ILogger<ProductService> logger,
    PricingEngine pricingEngine)
{
    public async Task<Product?> GetProductAsync(ProductId id)
    {
        logger.LogInformation("Fetching product {ProductId}", id);
        return await repository.GetByIdAsync(id);
    }
    
    public async Task<IEnumerable<Product>> GetProductsByCategoryAsync(string category)
    {
        logger.LogInformation("Fetching products in category {Category}", category);
        var products = await repository.GetByCategoryAsync(category);
        
        // Apply dynamic pricing
        foreach (var product in products)
        {
            var adjustedPrice = pricingEngine.CalculatePrice(product);
            logger.LogDebug("Product {Name}: {Original} -> {Adjusted}", 
                product.Name, product.Price, adjustedPrice);
        }
        
        return products;
    }
    
    public async Task<Product> CreateProductAsync(CreateProductRequest request)
    {
        var product = new Product(
            ProductId.NewGuid(),
            request.Name,
            request.Price,
            request.Category,
            request.InitialStock
        );
        
        await repository.AddAsync(product);
        logger.LogInformation("Created product {ProductId}: {Name}", product.Id, product.Name);
        
        return product;
    }
}

// ==================== Collection Expressions (C# 12) ====================

public class CollectionExamples
{
    // Collection expression for array initialization
    public static int[] GetPrimeNumbers() => [2, 3, 5, 7, 11, 13, 17, 19, 23];
    
    // Collection expression with spread operator
    public static int[] CombineArrays(int[] first, int[] second) => [..first, ..second];
    
    // Collection expression for List<T>
    public static List<string> GetDefaultCategories() => 
        ["Electronics", "Clothing", "Books", "Home & Garden", "Sports"];
    
    // Spread with filtering
    public static int[] GetEvenNumbers(int[] source) =>
        [..source.Where(n => n % 2 == 0)];
    
    // Empty collection expression
    public static List<Product> GetEmptyProductList() => [];
    
    // Collection expression in method calls
    public void ProcessItems()
    {
        ProcessBatch([1, 2, 3, 4, 5]);
        ProcessBatch([..GetPrimeNumbers(), 29, 31]);
    }
    
    private void ProcessBatch(int[] items) { /* ... */ }
}

// ==================== Default Lambda Parameters (C# 12) ====================

public class LambdaExamples
{
    // Lambda with default parameter
    public Func<decimal, decimal, decimal> CalculateTax = 
        (amount, rate = 0.08m) => amount * rate;
    
    // Lambda with multiple defaults
    public Func<string, int, bool, string> FormatMessage = 
        (message, repeat = 1, uppercase = false) =>
        {
            var result = string.Concat(Enumerable.Repeat(message, repeat));
            return uppercase ? result.ToUpper() : result;
        };
    
    public void DemoLambdaDefaults()
    {
        var tax1 = CalculateTax(100m);        // Uses default rate 0.08
        var tax2 = CalculateTax(100m, 0.10m); // Uses explicit rate 0.10
        
        var msg1 = FormatMessage("Hello");           // "Hello"
        var msg2 = FormatMessage("Hi", 3);           // "HiHiHi"
        var msg3 = FormatMessage("Test", 2, true);   // "TESTTEST"
    }
}

// ==================== Native AOT Compatible JSON Serialization ====================

[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSerializable(typeof(CreateProductRequest))]
[JsonSerializable(typeof(ApiResponse<Product>))]
[JsonSerializable(typeof(ApiResponse<List<Product>>))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = false)]
public partial class AppJsonContext : JsonSerializerContext { }

// ==================== Request/Response Models ====================

public record CreateProductRequest(
    string Name,
    decimal Price,
    string Category,
    int InitialStock = 0);

public record ApiResponse<T>(
    bool Success,
    T? Data,
    string? Error = null,
    DateTime Timestamp = default)
{
    public DateTime Timestamp { get; init; } = Timestamp == default 
        ? DateTime.UtcNow 
        : Timestamp;
    
    public static ApiResponse<T> Ok(T data) => new(true, data);
    public static ApiResponse<T> Fail(string error) => new(false, default, error);
}

// ==================== Minimal API with Native AOT ====================

public static class ProductEndpoints
{
    public static void MapProductEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products");
        
        group.MapGet("/", GetAllProducts)
            .WithName("GetAllProducts")
            .WithOpenApi();
        
        group.MapGet("/{id:guid}", GetProductById)
            .WithName("GetProductById")
            .WithOpenApi();
        
        group.MapPost("/", CreateProduct)
            .WithName("CreateProduct")
            .WithOpenApi();
        
        group.MapGet("/category/{category}", GetProductsByCategory)
            .WithName("GetProductsByCategory")
            .WithOpenApi();
    }
    
    private static async Task<IResult> GetAllProducts(
        ProductService service)
    {
        var products = await service.GetProductsByCategoryAsync("*");
        return Results.Ok(ApiResponse<IEnumerable<Product>>.Ok(products));
    }
    
    private static async Task<IResult> GetProductById(
        ProductId id,
        ProductService service)
    {
        var product = await service.GetProductAsync(id);
        
        return product is not null
            ? Results.Ok(ApiResponse<Product>.Ok(product))
            : Results.NotFound(ApiResponse<Product>.Fail("Product not found"));
    }
    
    private static async Task<IResult> CreateProduct(
        CreateProductRequest request,
        ProductService service)
    {
        var product = await service.CreateProductAsync(request);
        return Results.Created(
            $"/api/products/{product.Id}",
            ApiResponse<Product>.Ok(product));
    }
    
    private static async Task<IResult> GetProductsByCategory(
        string category,
        ProductService service)
    {
        var products = await service.GetProductsByCategoryAsync(category);
        return Results.Ok(ApiResponse<IEnumerable<Product>>.Ok(products));
    }
}

// ==================== Interfaces ====================

public interface IProductRepository
{
    Task<Product?> GetByIdAsync(ProductId id);
    Task<IEnumerable<Product>> GetByCategoryAsync(string category);
    Task AddAsync(Product product);
}

// ==================== Supporting Classes ====================

public class PricingEngine(ILogger<PricingEngine> logger)
{
    private readonly PriceMap _categoryMultipliers = new()
    {
        ["Electronics"] = 1.0m,
        ["Clothing"] = 0.95m,
        ["Books"] = 0.90m
    };
    
    public decimal CalculatePrice(Product product)
    {
        var multiplier = _categoryMultipliers.GetValueOrDefault(product.Category, 1.0m);
        var adjustedPrice = product.Price * multiplier;
        
        logger.LogDebug("Price calculation: {Original} * {Multiplier} = {Adjusted}",
            product.Price, multiplier, adjustedPrice);
        
        return adjustedPrice;
    }
}

public record OrderItem(ProductId ProductId, int Quantity, decimal UnitPrice);

// ==================== Program Entry Point ====================

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateSlimBuilder(args);
        
        // Configure JSON serialization for Native AOT
        builder.Services.ConfigureHttpJsonOptions(options =>
        {
            options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
        });
        
        // Register services with primary constructor DI
        builder.Services.AddSingleton<PricingEngine>();
        builder.Services.AddScoped<ProductService>();
        builder.Services.AddScoped<IProductRepository, InMemoryProductRepository>();
        
        var app = builder.Build();
        
        // Map endpoints
        app.MapProductEndpoints();
        
        app.Run();
    }
}

// In-memory repository for demo
public class InMemoryProductRepository : IProductRepository
{
    private readonly List<Product> _products = 
    [
        new(Guid.NewGuid(), "Laptop", 999.99m, "Electronics", 50),
        new(Guid.NewGuid(), "T-Shirt", 29.99m, "Clothing", 200),
        new(Guid.NewGuid(), "C# in Depth", 49.99m, "Books", 75)
    ];
    
    public Task<Product?> GetByIdAsync(ProductId id) =>
        Task.FromResult(_products.FirstOrDefault(p => p.Id == id));
    
    public Task<IEnumerable<Product>> GetByCategoryAsync(string category) =>
        Task.FromResult<IEnumerable<Product>>(
            category == "*" 
                ? _products 
                : _products.Where(p => p.Category == category).ToList());
    
    public Task AddAsync(Product product)
    {
        _products.Add(product);
        return Task.CompletedTask;
    }
}

Performance Improvements and Benchmarks

.NET 8 delivers measurable performance improvements across the stack. JSON serialization with source generators is up to 40% faster than reflection-based alternatives. LINQ operations benefit from vectorization improvements, particularly for numeric operations on arrays and spans. The garbage collector’s Dynamic PGO (Profile-Guided Optimization) improves steady-state throughput by learning application behavior at runtime.

Memory efficiency improvements reduce allocation pressure in common scenarios. Frozen collections provide immutable, highly-optimized dictionaries and sets for read-heavy workloads. SearchValues enables efficient multi-value searching in strings and spans. These primitives enable writing high-performance code without sacrificing readability.

.NET 8 Architecture - showing Native AOT, Blazor United, and C# 12 features
.NET 8 Architecture – Illustrating Native AOT compilation, Blazor United rendering modes, and C# 12 language features integration.

Key Takeaways and Migration Strategy

.NET 8 represents a significant step forward for the platform, combining performance improvements with developer productivity enhancements. Native AOT enables new deployment scenarios with minimal footprint. C# 12’s primary constructors and collection expressions reduce boilerplate while improving code clarity. Blazor United provides a cohesive full-stack web development experience.

For migration, start with updating target frameworks and addressing breaking changes. Adopt C# 12 features incrementally, beginning with primary constructors for new classes. Evaluate Native AOT for performance-critical services, particularly containerized microservices and serverless functions. The LTS designation ensures five years of support, making .NET 8 a solid foundation for enterprise applications.


Discover more from Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.