Making Your .NET Application Trim and AOT Ready

IL trimming removes unused code from your published application, reducing deployment size. Native AOT goes further — it compiles your application to a native binary with no JIT, no IL, and no runtime code generation. Both require your code (and its dependencies) to be analysable at build time. If your application uses reflection, dynamic loading, or runtime code generation, you'll need to make changes.

Enabling trimming

Add these properties to your project file:

MyApp.csproj
<PropertyGroup>
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>full</TrimMode>
</PropertyGroup>

TrimMode="full" trims all assemblies, including framework ones. The alternative, TrimMode="partial", only trims assemblies that explicitly opt in with [AssemblyMetadata("IsTrimmable", "True")].

Publish with:

dotnet publish -c Release

Enabling Native AOT

MyApp.csproj
<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>

Native AOT implies trimming — you don't need both properties. Publish with the same command, but note that AOT compilation requires platform-specific toolchains (MSVC on Windows, clang on Linux).

The trimmer's enemy: reflection

The trimmer analyses your code statically. It follows method calls, type references, and constructor invocations to determine what's reachable. Reflection defeats this analysis because the trimmer can't know at build time what types Type.GetType("MyApp.SomeClass") will resolve to.

Common reflection patterns that cause trimming failures:

Example.cs
// The trimmer can't see that MyService is needed
var type = Type.GetType("MyApp.Services.MyService");
var instance = Activator.CreateInstance(type!);

// The trimmer may remove the property
var prop = typeof(Order).GetProperty("Total");

// Dynamic assembly loading
Assembly.LoadFrom("plugin.dll");

Trim warnings

The compiler emits warnings for code patterns that are incompatible with trimming. Enable them:

config.xml
<PropertyGroup>
    <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>

Key warning codes to watch for:

Annotating your code

When you genuinely need reflection, annotate types so the trimmer preserves them:

Example.cs
public class PluginLoader
{
    public T CreatePlugin<[DynamicallyAccessedMembers(
        DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
        where T : class
    {
        return (T)Activator.CreateInstance(typeof(T))!;
    }
}

The DynamicallyAccessedMembers attribute tells the trimmer to preserve the specified members on the type argument.

For methods that fundamentally require runtime code generation:

Example.cs
[RequiresUnreferencedCode("This method uses reflection to discover handlers.")]
[RequiresDynamicCode("This method generates types at runtime.")]
public void RegisterHandlers()
{
    // Reflection-heavy code
}

These attributes propagate warnings to callers, making the trim-incompatibility visible up the call chain.

Source generators: the AOT-friendly alternative

The .NET ecosystem is systematically replacing reflection-based patterns with source generators:

Reflection-based Source-generated alternative
System.Text.Json (default) JsonSerializerContext source gen
Configuration binding Configuration binding source gen
Logging (ILogger) LoggerMessage source gen
Regex GeneratedRegex
HttpClientFactory Request/response type registration

Adopting these source generators is the single most impactful step for AOT readiness.

Checking library compatibility

Not all NuGet packages are trim-compatible. Check a package's status:

  1. Look for IsTrimmable in the package metadata
  2. Check the package's documentation for AOT support
  3. Build with trimming enabled and check for warnings referencing the package

The dotnet platform libraries are fully annotated. Popular third-party libraries vary — check their GitHub issues for AOT tracking issues.

Testing a trimmed application

Trimming bugs are runtime failures — a method that was trimmed away will throw a MissingMethodException or TypeLoadException. Your test strategy should include:

  1. Build with all warnings as errors for trim-related codes:
config.xml
<PropertyGroup>
    <WarningsAsErrors>IL2026;IL2057;IL2067;IL2075</WarningsAsErrors>
</PropertyGroup>
  1. Run integration tests against the published output, not the debug build. Trimming only happens during dotnet publish.

  2. Test all code paths, not just the happy path. A rarely-used error handler that uses reflection will only fail when that error actually occurs.

The road to AOT

A practical adoption path:

  1. Enable trim warnings and fix them — this is non-destructive and works in your normal build.
  2. Switch to source generators for JSON, logging, and configuration.
  3. Publish with trimming and run your full test suite against the trimmed output.
  4. Once clean, enable PublishAot and address any remaining RequiresDynamicCode warnings.

Not every application can be AOT-compiled today. EF Core, for instance, uses extensive runtime code generation and isn't fully AOT-compatible. But preparing your code for trimming — even if you never actually publish trimmed — improves code quality by making dependencies explicit and eliminating hidden reflection.