| name | dotnet-maui-aot |
| description | Optimizes MAUI for iOS/Catalyst. Native AOT pipeline, size/startup gains, library gaps, trimming. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
dotnet-maui-aot
Native AOT compilation for .NET MAUI on iOS and Mac Catalyst: compilation pipeline, publish profiles, up to 50% app size reduction and up to 50% startup improvement, library compatibility gaps, opt-out mechanisms, trimming interplay (RD.xml, source generators), and testing AOT builds on device.
Version assumptions: .NET 8.0+ baseline. Native AOT for MAUI is available on iOS and Mac Catalyst. Android uses a different compilation model (CoreCLR in .NET 11, Mono/AOT in .NET 8-10).
Scope
- iOS/Mac Catalyst AOT compilation pipeline
- Publish profile configuration for MAUI AOT
- Size/startup improvement measurements
- Library compatibility gaps for MAUI AOT apps
- Opt-out mechanisms and trimming interplay
- Testing AOT builds on device
Out of scope
- MAUI development patterns (project structure, XAML, MVVM) -- see [skill:dotnet-maui-development]
- MAUI testing -- see [skill:dotnet-maui-testing]
- WASM AOT (Blazor/Uno) -- see [skill:dotnet-aot-wasm]
- General AOT architecture -- see [skill:dotnet-native-aot]
Cross-references: [skill:dotnet-maui-development] for MAUI patterns, [skill:dotnet-maui-testing] for testing AOT builds, [skill:dotnet-native-aot] for general AOT patterns, [skill:dotnet-aot-wasm] for WASM AOT, [skill:dotnet-ui-chooser] for framework selection.
iOS/Mac Catalyst AOT Compilation Pipeline
Native AOT on iOS and Mac Catalyst compiles .NET IL directly to native machine code at publish time, eliminating the need for a JIT compiler or IL interpreter at runtime. This produces a self-contained native binary that links against platform frameworks.
How It Works
- IL compilation: The .NET IL is compiled to native code by the NativeAOT compiler (ILC)
- Tree shaking: Unused code is trimmed based on static analysis of reachable types and methods
- Native linking: The generated native code is linked with iOS/Catalyst frameworks and the minimal .NET runtime
- App bundle: The result is a standard
.app bundle with a native executable (no IL assemblies shipped)
Publish Configuration
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0-ios' Or
'$(TargetFramework)' == 'net8.0-maccatalyst'">
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
dotnet publish -f net8.0-ios -c Release -r ios-arm64
dotnet publish -f net8.0-maccatalyst -c Release -r maccatalyst-arm64
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64
Entitlements and Provisioning
AOT builds require the same entitlements and provisioning profiles as regular iOS/Catalyst builds. No additional entitlements are needed for AOT specifically.
Size Reduction
Native AOT can achieve up to 50% app size reduction compared to interpreter/JIT mode on iOS. The size improvement comes from:
- Tree shaking: Only reachable code is included in the final binary
- No IL shipping: The app bundle does not include .NET IL assemblies
- No runtime JIT: The JIT compiler and associated metadata are not packaged
Typical Size Comparison
| Mode | Approximate Size | Notes |
|---|
| Interpreter (default .NET 8 iOS) | ~60-80 MB | Includes IL assemblies + interpreter |
| Native AOT | ~30-45 MB | Native binary only, no IL |
| Native AOT + StripSymbols | ~25-40 MB | Debug symbols stripped |
Caveat: Actual size reduction depends on app complexity, third-party library usage, and how much code is reachable after trimming. Libraries that use heavy reflection may prevent aggressive trimming and reduce size gains.
Startup Improvement
Native AOT provides up to 50% faster cold startup on iOS and Mac Catalyst. The startup improvement comes from:
- No JIT warmup: Code is already native; no compilation at app launch
- No IL loading: No need to load and parse .NET assemblies
- Reduced memory pressure: Smaller working set during startup
Measuring Startup
public partial class App : Application
{
private static readonly long StartTicks = Stopwatch.GetTimestamp();
public App()
{
InitializeComponent();
MainPage = new AppShell();
var elapsed = Stopwatch.GetElapsedTime(StartTicks);
System.Diagnostics.Debug.WriteLine(
$"App startup: {elapsed.TotalMilliseconds:F0}ms");
}
}
Library Compatibility
Many .NET libraries are not fully AOT-compatible. Common compatibility issues stem from:
- Reflection: Runtime type inspection,
Type.GetType(), Activator.CreateInstance()
- Dynamic code generation:
System.Reflection.Emit, System.Linq.Expressions.Compile()
- Serialization without source generators: JSON/XML serializers that use reflection
Compatibility Matrix
| Library / Feature | AOT Status | Workaround |
|---|
| System.Text.Json (source gen) | Compatible | Use [JsonSerializable] context |
| System.Text.Json (reflection) | Breaks | Switch to source generators |
| CommunityToolkit.Mvvm | Compatible | Source-gen based, AOT-safe |
| Entity Framework Core | Partial | Precompiled queries; no dynamic LINQ |
| Newtonsoft.Json | Breaks | Migrate to System.Text.Json with source gen |
| AutoMapper | Breaks | Use Mapperly (source gen) |
| MediatR | Partial | Register handlers explicitly, avoid assembly scanning |
| HttpClient | Compatible | Standard usage works |
| MAUI Essentials | Compatible | Platform APIs are AOT-safe |
| SQLite-net | Compatible | Uses P/Invoke, AOT-safe |
| Refit | Breaks | Use Refit 7+ (includes source generator; enable with [GenerateRefitClient]) |
| FluentValidation | Partial | Avoid runtime expression compilation |
Detecting Incompatible Code
<PropertyGroup>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
AOT analysis produces warnings like IL3050 (RequiresDynamicCode) and IL2026 (RequiresUnreferencedCode). Address these before publishing with AOT.
Opt-Out Mechanisms
Disabling AOT Entirely
<PropertyGroup>
<PublishAot>false</PublishAot>
</PropertyGroup>
Per-Assembly Trimming Overrides
When a specific library is not AOT-compatible, you can preserve it from trimming while still using AOT for the rest of the app:
<ItemGroup>
<TrimmerRootAssembly Include="IncompatibleLibrary" />
</ItemGroup>
Opt-Out of .NET 11 Defaults
.NET 11 introduces new defaults that interact with AOT:
<PropertyGroup>
<MauiXamlInflator>XamlC</MauiXamlInflator>
</PropertyGroup>
<PropertyGroup>
<UseMonoRuntime>true</UseMonoRuntime>
</PropertyGroup>
Trimming Interplay
Native AOT requires trimming. When PublishAot is true, trimming is automatically enabled. Understanding trimming configuration is essential for a successful AOT build.
ILLink Descriptors for Reflection Preservation
Note: In Xamarin/Mono-era documentation, these were called "rd.xml" (Runtime Directives). In .NET 8+ Native AOT, use ILLink descriptor XML files instead.
When code uses reflection that the trimmer cannot statically analyze, use an ILLink descriptor XML file to preserve types. You can also use [DynamicDependency] attributes for fine-grained preservation in code.
ILLink descriptor XML (preferred for bulk preservation):
<linker>
<assembly fullname="MyApp">
<type fullname="MyApp.Models.LegacyConfig" preserve="all" />
<type fullname="MyApp.Services.PluginLoader">
<method name="LoadPlugin" />
</type>
</assembly>
<assembly fullname="IncompatibleLibrary" preserve="all" />
</linker>
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
[DynamicDependency] attribute (preferred for targeted preservation):
using System.Diagnostics.CodeAnalysis;
[DynamicDependency(nameof(LegacyConfig.Initialize), typeof(LegacyConfig))]
public void ConfigureApp() { }
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(LegacyConfig))]
public void LoadPlugins() { }
Source Generator Alternatives
When source generators aren't available, use [DynamicDependency] attributes (shown above) for targeted preservation without ILLink XML files.
Prefer source generators over reflection to avoid trimming issues entirely:
| Reflection Pattern | Source Generator Alternative |
|---|
JsonSerializer.Deserialize<T>() | [JsonSerializable] context (System.Text.Json) |
Activator.CreateInstance<T>() | Factory pattern with explicit registration |
Type.GetProperties() | CommunityToolkit.Mvvm [ObservableProperty] |
| Assembly scanning for DI | Explicit services.Add*() registrations |
| AutoMapper reflection mapping | Mapperly [Mapper] source generator |
Trimming Warnings
dotnet publish -f net8.0-ios -c Release /p:PublishAot=true /p:TrimmerSingleWarn=false
Common trim warnings:
- IL2026: Member with
RequiresUnreferencedCode -- the member does something not guaranteed to work after trimming
- IL2046: Trim attribute mismatch between base/derived types
- IL3050: Member with
RequiresDynamicCode -- the member generates code at runtime (incompatible with AOT)
Testing AOT Builds
AOT builds can behave differently from Debug/JIT builds. Always test on a real device or simulator with an AOT-published build before release.
Common AOT-Only Failures
| Failure | Symptom | Fix |
|---|
| Missing type metadata | MissingMetadataException at runtime | Add type to ILLink descriptor or use [DynamicDependency] |
| Trimmed method | MissingMethodException | Add [DynamicDependency] or ILLink descriptor entry |
| Dynamic code gen | PlatformNotSupportedException | Replace with source generator alternative |
| Reflection-based serialization | Empty/null deserialized objects | Use [JsonSerializable] source gen |
| Assembly scanning | Missing services at runtime | Register services explicitly in DI |
Testing Workflow
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64
dotnet publish -f net8.0-ios -c Release -r ios-arm64
CI Integration
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64 /p:PublishAot=true
xharness apple test \
--app bin/Release/net8.0-ios/iossimulator-arm64/publish/MyApp.app \
--target ios-simulator-64 \
--timeout 00:10:00 \
--output-directory test-results/aot
For MAUI testing patterns (Appium, XHarness), see [skill:dotnet-maui-testing].
Agent Gotchas
- Do not enable
PublishAot without also enabling trim analyzers. AOT requires trimming. Set <EnableTrimAnalyzer>true</EnableTrimAnalyzer> and <EnableAotAnalyzer>true</EnableAotAnalyzer> during development to catch issues early.
- Do not assume all NuGet packages are AOT-compatible. Check for
IsAotCompatible in the package's .csproj or look for trim/AOT warnings when building. Many popular packages still use reflection internally.
- Do not use
Newtonsoft.Json with AOT. It relies entirely on reflection. Migrate to System.Text.Json with [JsonSerializable] source gen contexts for AOT-safe serialization.
- Do not skip device testing for AOT builds. Simulator testing catches most issues, but physical device behavior can differ -- especially for startup timing, memory constraints, and platform service integration.
- Do not confuse MAUI iOS AOT with Android AOT. MAUI Native AOT (
PublishAot) targets iOS and Mac Catalyst only. Android uses a different compilation model (Mono AOT in .NET 8-10, CoreCLR in .NET 11+). They are configured separately.
Prerequisites
- .NET 8.0+ with MAUI workload
- Xcode and iOS/Mac Catalyst SDKs (macOS only)
- Apple Developer account for physical device deployment
- Provisioning profile and signing certificate for device testing
References