Skill v1.0.1
currentAutomated scan100/1002 files
name: dotnet-aot-architecture description: "Designing AOT-first apps. Source gen over reflection, AOT-safe DI, serialization, factories." metadata: version: "1.0.1"
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).
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-safebuilder.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 AOTbuilder.Services.Scan(scan => scan.FromAssemblyOf<OrderService>().AddClasses(classes => classes.AssignableTo<IService>()).AsImplementedInterfaces().WithScopedLifetime());// GOOD: Explicit registrations grouped by concernbuilder.Services.AddOrderServices();builder.Services.AddInventoryServices();// Extension method groups related registrationspublic 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 registrationbuilder.Services.AddKeyedSingleton<INotificationSender, EmailSender>("email");builder.Services.AddKeyedSingleton<INotificationSender, SmsSender>("sms");// Resolve by keyapp.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 Corebuilder.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 AOTpublic T CreateHandler<T>() where T : class=> (T)Activator.CreateInstance(typeof(T))!;// GOOD: Factory with explicit registrationpublic 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)]();}// Registrationvar factory = new HandlerFactory();factory.Register<OrderHandler>(() => new OrderHandler(repository, logger));factory.Register<PaymentHandler>(() => new PaymentHandler(gateway));
Strategy Pattern via DI
// BAD: Dynamic type resolutionpublic 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 reflectionapp.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 expressionpublic 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 enableddotnet 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 DIMyApp.csproj # PublishAot=true, EnableAotAnalyzer=trueJsonContext.cs # [JsonSerializable] for all API typesEndpoints/OrderEndpoints.cs # Minimal API route groupsProductEndpoints.csServices/OrderService.cs # Business logic (no reflection)IOrderService.csRepositories/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=trueand 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.