Native AOT and Trimming Support
ActualLab libraries support .NET Native AOT compilation and IL trimming. This guide covers the CodeKeeper infrastructure that prevents the trimmer from removing required code.
Overview
Fusion uses runtime code generation for:
- Proxy classes (for compute services, RPC services, commanders)
- Generic
ArgumentListimplementations - Method invocation infrastructure
When publishing with AOT or trimming, the .NET linker may remove code that appears unused at compile time but is needed at runtime. The CodeKeeper infrastructure solves this by registering "fake" usage of required types and methods.
RuntimeCodegen Modes
Fusion supports multiple code generation strategies:
| Mode | Description |
|---|---|
DynamicMethods | Runtime IL generation (default for JIT) |
InterpretedExpressions | Expression tree interpretation (AOT-compatible) |
CompiledExpressions | Compiled expression trees |
The mode is selected based on the runtime environment:
// Check the current mode
Console.WriteLine($"RuntimeCodegen.Mode: {RuntimeCodegen.Mode}");
// In Native AOT:
// - NativeMode will be InterpretedExpressions
// - DynamicMethods is not availableCodeKeeper Infrastructure
Base CodeKeeper Class
CodeKeeper is the base class for preventing trimming of required code:
public abstract class CodeKeeper
{
// Never evaluates to true at runtime, but compiler can't prove it
public static readonly bool AlwaysFalse;
public static readonly bool AlwaysTrue;
// Register a type to prevent trimming
public static T Keep<T>(bool ensureInitialized = false);
// Register serializable types
public static T KeepSerializable<T>();
// Run all registered actions (called at startup)
public static void RunActions();
}ProxyCodeKeeper Hierarchy
Specialized code keepers for each subsystem:
| Class | Purpose |
|---|---|
ProxyCodeKeeper | Base proxy and ArgumentList support |
CommanderProxyCodeKeeper | Commander command handlers |
RpcProxyCodeKeeper | RPC service methods |
FusionProxyCodeKeeper | Compute methods and Fusion-specific types |
Usage in Native AOT Applications
Basic Setup
At the start of your program, before any Fusion code runs:
// Set the code keeper to use (FusionProxyCodeKeeper includes all subsystems)
CodeKeeper.Set<ProxyCodeKeeper, FusionProxyCodeKeeper>();
// Run the code keeper actions to register types
if (RuntimeCodegen.NativeMode != RuntimeCodegenMode.DynamicMethods)
CodeKeeper.RunActions();Complete Example
From the NativeAot sample:
#pragma warning disable IL3050
public static async Task Main()
{
// Configure code keeper before anything else
CodeKeeper.Set<ProxyCodeKeeper, FusionProxyCodeKeeper>();
if (RuntimeCodegen.NativeMode != RuntimeCodegenMode.DynamicMethods)
CodeKeeper.RunActions();
// Now configure services as usual
var services = new ServiceCollection()
.AddLogging(l => l.AddSimpleConsole())
.AddFusion(fusion => {
fusion.Rpc.AddWebSocketClient();
fusion.AddServerAndClient<ITestService, TestService>();
})
.AddSingleton(_ => RpcOutboundCallOptions.Default with {
RouterFactory = methodDef => args => RpcPeerRef.Loopback,
})
.BuildServiceProvider();
// Use services
var client = services.RpcHub().GetClient<ITestService>();
var now = await client.GetTime();
}
#pragma warning restore IL3050Keeping Custom Types
For Service Methods
Register the types used in your service methods:
public class MyAppCodeKeeper : FusionProxyCodeKeeper
{
public MyAppCodeKeeper()
{
if (AlwaysTrue)
return;
// Keep types used as method results
KeepAsyncMethod<MyResult>(); // Task<MyResult>
KeepAsyncMethod<MyResult, string>(); // Task<MyResult> Method(string arg)
KeepAsyncMethod<MyResult, string, int>(); // Task<MyResult> Method(string, int)
// Keep types used as method arguments
KeepMethodArgument<MyCommand>();
// Keep serializable types
KeepSerializable<MyDto>();
}
}For Proxy Types
If you pre-generate proxy classes, keep them explicitly:
public class MyAppProxyCodeKeeper : FusionProxyCodeKeeper
{
public MyAppProxyCodeKeeper()
{
if (AlwaysTrue)
return;
// Keep service interface and its generated proxy
KeepProxy<IMyService, MyServiceProxy>();
}
}
// Use your custom code keeper
// CodeKeeper.Set<ProxyCodeKeeper, MyAppProxyCodeKeeper>();How CodeKeeper Works
CodeKeepers use a clever pattern to prevent trimming while avoiding runtime overhead:
public class MyCodeKeeper : CodeKeeper
{
public void KeepMyType()
{
// This condition is always false at runtime, but the compiler can't prove it
if (AlwaysTrue)
return;
// This code is never executed, but the trimmer sees the reference
// and preserves the type
Keep<MyType>();
}
}The AlwaysFalse/AlwaysTrue constants use runtime values that the compiler cannot evaluate:
public static readonly bool AlwaysFalse =
CpuTimestamp.Now.Value == -1 && RandomShared.NextDouble() < 1e-300;Project Configuration
Enable AOT Publishing
In your .csproj:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>Suppress Trimming Warnings
For code that uses reflection intentionally:
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "Types are preserved by CodeKeeper")]
[UnconditionalSuppressMessage("Trimming", "IL3050",
Justification = "Types are preserved by CodeKeeper")]
public static void MyMethod() { }DynamicallyAccessedMembers
For generic parameters that need reflection:
public static void ProcessType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
{
// T's members are preserved by the trimmer
}ArgumentList Support
ArgumentList requires special handling because it uses generic types based on argument count:
// These types are auto-generated and need preservation
var l0 = ArgumentList.New(); // ArgumentList0
var l2 = ArgumentList.New(1, "s"); // ArgumentList2<int, string>
var l10 = ArgumentList.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); // ArgumentList10<...>The ArgumentListCodeKeeper (invoked through ProxyCodeKeeper) preserves these types:
public void KeepArgumentListArgument<T>()
{
// Keeps ArgumentListN types that include T as a type argument
}Limitations
Dynamic proxy generation: Native AOT doesn't support
System.Reflection.Emitat runtime. Proxies must be either:- Pre-generated at build time (source generators)
- Using interpreted expressions mode
Expression tree limitations: Some complex expressions may not work in interpreted mode. Test your application in AOT mode during development.
Serialization: All serialized types must be annotated with appropriate attributes (
[DynamicallyAccessedMembers],[MemoryPackable], etc.)
Best Practices
Initialize early: Call
CodeKeeper.Set()andCodeKeeper.RunActions()at the very start of your application, before any DI containers are built.Use FusionProxyCodeKeeper: It includes all subsystems (Commander, RPC, Fusion). Only create a custom code keeper if you need to add application-specific types.
Test in AOT mode: Run
dotnet publish -c Releaseand test the AOT binary regularly during development to catch trimming issues early.Annotate serializable types: Use
[DynamicallyAccessedMembers]or serialization attributes on all types that may be serialized/deserialized.Check RuntimeCodegen.Mode: If your code behaves differently based on the codegen mode, log it at startup for easier debugging.
Related Topics
- Serialization - Type annotations for serialization
- Interceptors and Proxies - Proxy generation details
