dotnet-aot-architecture
AOT-first application design patterns for .NET 8+: preferring source generators over reflection, explicit DI registration over assembly scanning, AOT-safe serialization choices, library compatibility assessment, and factory patterns replacing Activator.CreateInstance .
Version assumptions: .NET 8.0+ baseline. Patterns apply to all AOT-capable project types (console, ASP.NET Core Minimal APIs, worker services).
Scope
-
Source generator replacements for reflection patterns
-
AOT-safe DI patterns (explicit registration, keyed services)
-
Serialization choices for AOT (STJ source gen, Protobuf, MessagePack)
-
Factory patterns replacing Activator.CreateInstance
-
Library compatibility assessment for AOT
-
AOT application architecture template
Out of scope
-
Native AOT publish pipeline and MSBuild configuration -- see [skill:dotnet-native-aot]
-
Trim-safe library authoring and annotations -- see [skill:dotnet-trimming]
-
WASM AOT compilation -- see [skill:dotnet-aot-wasm]
-
MAUI-specific AOT -- see [skill:dotnet-maui-aot]
-
Source generator authoring (Roslyn API) -- see [skill:dotnet-csharp-source-generators]
-
DI container internals -- see [skill:dotnet-csharp-dependency-injection]
-
Serialization depth -- see [skill:dotnet-serialization]
Cross-references: [skill:dotnet-native-aot] for the AOT publish pipeline, [skill:dotnet-trimming] for trim annotations and library authoring, [skill:dotnet-serialization] for serialization patterns, [skill:dotnet-csharp-source-generators] for source gen mechanics, [skill:dotnet-csharp-dependency-injection] for DI fundamentals, [skill:dotnet-containers] for runtime-deps deployment, [skill:dotnet-native-interop] for general P/Invoke patterns and marshalling.
Source Generators Over Reflection
The primary AOT enabler is replacing runtime reflection with compile-time source generation. Source generators produce code at build time that the AOT compiler can analyze and include.
Key Source Generator Replacements
Reflection Pattern Source Generator / AOT-Safe Alternative Library
JsonSerializer.Deserialize<T>()
[JsonSerializable] context System.Text.Json (built-in)
Activator.CreateInstance<T>()
Factory pattern with explicit new
Manual
Type.GetProperties() for mapping [Mapper] attribute Mapperly
Regex pattern compilation [GeneratedRegex] attribute Built-in (.NET 7+)
ILogger.Log(...) with string interpolation [LoggerMessage] attribute Microsoft.Extensions.Logging
Assembly scanning for DI Explicit services.Add*()
Manual
[DllImport] P/Invoke [LibraryImport]
Built-in (.NET 7+)
AutoMapper CreateMap<>()
[Mapper] source gen Mapperly
Example: Migrating to Source Gen
// BEFORE: Reflection-based (breaks under AOT) var logger = loggerFactory.CreateLogger<OrderService>(); logger.LogInformation("Order {OrderId} created for {Customer}", order.Id, order.CustomerId);
// AFTER: Source-generated (AOT-safe, zero-alloc) public partial class OrderService { [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} created for {Customer}")] private static partial void LogOrderCreated( ILogger logger, int orderId, string customer); }
// Usage: LogOrderCreated(_logger, order.Id, order.CustomerId);
See [skill:dotnet-csharp-source-generators] for source generator mechanics and authoring patterns.
AOT-Safe DI Patterns
Dependency injection in AOT requires explicit service registration. Assembly scanning (AddServicesFromAssembly ) and open-generic resolution may require reflection that AOT cannot satisfy.
Explicit Registration (Preferred)
var builder = WebApplication.CreateSlimBuilder(args);
// Explicit registrations -- AOT-safe builder.Services.AddSingleton<IOrderRepository, PostgresOrderRepository>(); builder.Services.AddScoped<IOrderService, OrderService>(); builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); builder.Services.AddSingleton(TimeProvider.System);
Avoid Assembly Scanning
// BAD: Assembly scanning uses reflection -- breaks under AOT builder.Services.Scan(scan => scan .FromAssemblyOf<OrderService>() .AddClasses(classes => classes.AssignableTo<IService>()) .AsImplementedInterfaces() .WithScopedLifetime());
// GOOD: Explicit registrations grouped by concern builder.Services.AddOrderServices(); builder.Services.AddInventoryServices();
// Extension method groups related registrations public static class OrderServiceExtensions { public static IServiceCollection AddOrderServices( this IServiceCollection services) { services.AddScoped<IOrderService, OrderService>(); services.AddScoped<IOrderRepository, PostgresOrderRepository>(); services.AddScoped<IOrderValidator, OrderValidator>(); return services; } }
Keyed Services (.NET 8+)
// AOT-safe keyed service registration builder.Services.AddKeyedSingleton<INotificationSender, EmailSender>("email"); builder.Services.AddKeyedSingleton<INotificationSender, SmsSender>("sms");
// Resolve by key app.MapPost("/notify", ([FromKeyedServices("email")] INotificationSender sender) => sender.SendAsync("Hello"));
See [skill:dotnet-csharp-dependency-injection] for full DI patterns.
Serialization Choices for AOT
Decision Matrix
Serializer AOT-Safe Setup Required Best For
System.Text.Json + source gen Yes [JsonSerializable] context APIs, config, JSON interop
Protobuf (Google.Protobuf) Yes .proto schema files gRPC, service-to-service
MessagePack + source gen Yes [MessagePackObject]
- source gen resolver Caching, real-time
Newtonsoft.Json No N/A Do not use for AOT
STJ without source gen No N/A Falls back to reflection
STJ Source Gen Setup
// Define serializable types [JsonSerializable(typeof(Product))] [JsonSerializable(typeof(List<Product>))] [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal partial class AppJsonContext : JsonSerializerContext { }
// Register in ASP.NET Core builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default); });
See [skill:dotnet-serialization] for comprehensive serialization patterns.
Factory Patterns Replacing Activator.CreateInstance
Activator.CreateInstance uses runtime reflection to create instances and is incompatible with AOT. Replace with factory patterns that use explicit construction.
Simple Factory
// BAD: Reflection-based creation -- breaks under AOT public T CreateHandler<T>() where T : class => (T)Activator.CreateInstance(typeof(T))!;
// GOOD: Factory with explicit registration public class HandlerFactory { private readonly Dictionary<Type, Func<IHandler>> _factories = new();
public void Register<T>(Func<T> factory) where T : IHandler
=> _factories[typeof(T)] = () => factory();
public IHandler Create<T>() where T : IHandler
=> _factories[typeof(T)]();
}
// Registration var factory = new HandlerFactory(); factory.Register<OrderHandler>(() => new OrderHandler(repository, logger)); factory.Register<PaymentHandler>(() => new PaymentHandler(gateway));
Strategy Pattern via DI
// BAD: Dynamic type resolution public IPaymentProcessor GetProcessor(string type) { var processorType = Type.GetType($"MyApp.Payments.{type}Processor"); return (IPaymentProcessor)Activator.CreateInstance(processorType!)!; }
// GOOD: Keyed services (.NET 8+) builder.Services.AddKeyedScoped<IPaymentProcessor, CreditCardProcessor>("CreditCard"); builder.Services.AddKeyedScoped<IPaymentProcessor, BankTransferProcessor>("BankTransfer"); builder.Services.AddKeyedScoped<IPaymentProcessor, WalletProcessor>("Wallet");
// Resolve at runtime without reflection app.MapPost("/pay", ( [FromQuery] string type, IServiceProvider sp) => { var processor = sp.GetRequiredKeyedService<IPaymentProcessor>(type); return processor.ProcessAsync(); });
Enum-Based Factory
// For a fixed set of types, use a switch expression public static IExporter CreateExporter(ExportFormat format) => format switch { ExportFormat.Csv => new CsvExporter(), ExportFormat.Json => new JsonExporter(), ExportFormat.Pdf => new PdfExporter(), _ => throw new ArgumentOutOfRangeException(nameof(format)) };
Library Compatibility Assessment
Assessment Checklist
Before adopting a NuGet package in an AOT project:
-
Check for IsAotCompatible in the package source -- packages that set this are validated against AOT analyzers
-
Check for [RequiresDynamicCode] / [RequiresUnreferencedCode] annotations -- these indicate AOT-incompatible APIs
-
Run AOT analyzers against your usage -- dotnet build /p:EnableAotAnalyzer=true
-
Check the package's GitHub issues for AOT/trimming reports -- search for "Native AOT", "trimming", "IL2026", "IL3050"
-
Look for source-generated alternatives -- many reflection-based libraries now have source-gen companions
Common Library Status
Library AOT Status AOT-Safe Alternative
AutoMapper Breaks Mapperly
MediatR Partial (explicit registration) Direct method calls or factory
FluentValidation Partial Manual validation or source gen
Dapper Compatible (.NET 8+ AOT support)
Entity Framework Core Partial (precompiled queries) Dapper for AOT-heavy paths
Refit Compatible (7+ with source gen)
Polly Compatible (v8+)
Serilog Partial [LoggerMessage] source gen
Hangfire Breaks Custom IHostedService
Testing Compatibility
Build with all analyzers enabled
dotnet build /p:EnableAotAnalyzer=true /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false
Warnings indicate AOT-incompatible usage
IL3050 = RequiresDynamicCode (definitely breaks)
IL2026 = RequiresUnreferencedCode (may break)
AOT Application Architecture Template
src/ MyApp/ Program.cs # CreateSlimBuilder, explicit DI MyApp.csproj # PublishAot=true, EnableAotAnalyzer=true JsonContext.cs # [JsonSerializable] for all API types Endpoints/ OrderEndpoints.cs # Minimal API route groups ProductEndpoints.cs Services/ OrderService.cs # Business logic (no reflection) IOrderService.cs Repositories/ OrderRepository.cs # Data access (Dapper or EF precompiled) Extensions/ ServiceCollectionExtensions.cs # Grouped DI registrations
Agent Gotchas
-
Do not use Activator.CreateInstance in AOT projects. It requires runtime reflection that is not available. Use factory patterns, DI keyed services, or switch expressions instead.
-
Do not use assembly scanning for DI registration (Scan , RegisterAssemblyTypes , FromAssemblyOf ). These use reflection to discover types at runtime. Register services explicitly.
-
Do not use System.Text.Json without a [JsonSerializable] context in AOT. Without a source-generated context, STJ falls back to reflection and fails at runtime.
-
Do not assume a library is AOT-compatible without testing. Run dotnet build /p:EnableAotAnalyzer=true and check for IL3050/IL2026 warnings against your specific usage.
-
Do not use Type.GetType() or Assembly.GetTypes() for runtime discovery. These rely on metadata that may be trimmed. Use compile-time known types.
References
-
Native AOT deployment
-
Source generation in .NET
-
System.Text.Json source generation
-
LoggerMessage source generation
-
Mapperly object mapper
-
Prepare .NET libraries for trimming