-
Notifications
You must be signed in to change notification settings - Fork 0
AOT and Trim Compatibility
The net10.0 build of FunctionalStateMachine.Core and FunctionalStateMachine.CommandRunner is fully compatible with:
-
NativeAOT (
PublishAot=true) — compiled ahead of time to a self-contained native binary -
Trimming (
PublishTrimmed=true) — unused code removed at publish time to reduce binary size -
Single-file publishing (
PublishSingleFile=true)
| Package | IsAotCompatible |
Reflection-free | Trim-safe |
|---|---|---|---|
FunctionalStateMachine.Core |
✅ (net10.0) |
✅ | ✅ |
FunctionalStateMachine.CommandRunner |
✅ (net10.0) |
✅ | ✅ |
FunctionalStateMachine.Diagrams |
N/A (build-time only) | N/A | N/A |
FunctionalStateMachine.Core.Generator |
N/A (build-time only) | N/A | N/A |
FunctionalStateMachine.CommandRunner.Generator |
N/A (build-time only) | N/A | N/A |
The two generators (Core.Generator and CommandRunner.Generator) are Roslyn analyzers that run at compile time inside the compiler process, not in your application. They are never published as part of your app binary.
Add <PublishTrimmed>true</PublishTrimmed> to your .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
</Project>The source generator is bundled inside the FunctionalStateMachine.Core NuGet package and applied automatically — no extra package reference needed.
Then publish:
dotnet publish -c Release -r linux-x64 --sc true<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>dotnet publish -c Release -r linux-x64The library was originally written to warn about unused trigger types at state machine build time. This required knowing all defined trigger subtypes — which was initially discovered via Assembly.GetTypes() and Type.GetProperty() (reflection).
Both reflection APIs are incompatible with AOT/trimming because the trimmer may remove types it doesn't see referenced, and Assembly.GetTypes() only returns what survives trimming.
The FunctionalStateMachine.Core.Generator Roslyn source generator solves this at compile time:
- It detects all
StateMachine<…, TTrigger, …>.Create()call sites in your compilation. - For each unique
TTrigger, it finds all non-abstract concrete derived types using the Roslyn symbol API (no runtime reflection). - It generates a
[ModuleInitializer]that registers those types inTriggerTypeRegistrybefore any app code runs.
// Example of generated output (in your bin/obj folder):
// <auto-generated />
[ModuleInitializer]
internal static void Initialize()
{
TriggerTypeRegistry.Register<global::MyApp.OrderTrigger>(new[]
{
typeof(global::MyApp.OrderTrigger.Process),
typeof(global::MyApp.OrderTrigger.Cancel),
typeof(global::MyApp.OrderTrigger.Complete),
});
}At runtime, AnalyzeUnusedTriggers reads from this registry — no assembly scanning, no reflection. If the registry has not been populated (e.g. the generator wasn't active for that trigger type), the check is silently skipped rather than throwing.
FunctionalStateMachine.CommandRunner also uses a source generator (CommandRunner.Generator) to produce a type-switching dispatcher at compile time. The dispatcher uses a switch on concrete command types rather than any reflection-based dispatch, making it fully AOT- and trim-safe.
When several state machines in the same project share the same TTrigger, the generator registers the trigger types once (deduplicated by trigger base type). Each machine's unused-trigger analysis runs independently at .Build() time:
// Both machines share OrderTrigger; generator registers its types once.
// machineA may use all triggers; machineB may only use a subset.
// Each machine independently warns about its own unused triggers.
var machineA = StateMachine<StateA, OrderTrigger, DataA, CmdA>.Create()
.For(StateA.Idle)
.On<OrderTrigger.Process>() // uses Process
.TransitionTo(StateA.Done)
// ⚠️ warning: Cancel and Complete unused in this machine
.Build();
var machineB = StateMachine<StateB, OrderTrigger, DataB, CmdB>.Create()
.For(StateB.Idle)
.On<OrderTrigger.Cancel>() // uses Cancel
.TransitionTo(StateB.Cancelled)
// ⚠️ warning: Process and Complete unused in this machine
.Build();Both machines build successfully — unused-trigger warnings are informational and never block construction.