-
Notifications
You must be signed in to change notification settings - Fork 0
AOT/trim compatibility: source-generated trigger registry, sample publishing, multi-machine tests, and compatibility docs #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0e6c9bf
Initial plan
Copilot f37ffc0
feat: add AOT and trim compatibility to Core and CommandRunner libraries
Copilot f256760
feat: replace reflection in StateMachineAnalysis with source-generate…
Copilot 65bc37e
feat: multiple-machine/sub-machine tests, AOT trim fix for samples, C…
Copilot 5fc2462
feat: add missing multi-machine unused-trigger test and AOT/compat docs
Copilot 43ffdf0
docs: simplify AOT doc to NuGet-only perspective, remove project-refe…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # AOT and Trim Compatibility | ||
|
|
||
| The `net8.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`) | ||
|
|
||
| ## Status | ||
|
|
||
| | Package | `IsAotCompatible` | Reflection-free | Trim-safe | | ||
| |---|---|---|---| | ||
| | `FunctionalStateMachine.Core` | ✅ (`net8.0+`) | ✅ | ✅ | | ||
| | `FunctionalStateMachine.CommandRunner` | ✅ (`net8.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. | ||
|
|
||
| ## Enabling publishing with trimming | ||
|
|
||
| ### Executable projects | ||
|
|
||
| Add `<PublishTrimmed>true</PublishTrimmed>` to your `.csproj`: | ||
|
|
||
| ```xml | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net9.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: | ||
|
|
||
| ```bash | ||
| dotnet publish -c Release -r linux-x64 --sc true | ||
| ``` | ||
|
|
||
| ### NativeAOT | ||
|
|
||
| ```xml | ||
| <PropertyGroup> | ||
| <PublishAot>true</PublishAot> | ||
| </PropertyGroup> | ||
| ``` | ||
|
|
||
| ```bash | ||
| dotnet publish -c Release -r linux-x64 | ||
| ``` | ||
|
|
||
| ## How it works: no reflection at runtime | ||
|
|
||
| The 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 source generator approach | ||
|
|
||
| The `FunctionalStateMachine.Core.Generator` Roslyn source generator solves this at compile time: | ||
|
|
||
| 1. It detects all `StateMachine<…, TTrigger, …>.Create()` call sites in your compilation. | ||
| 2. For each unique `TTrigger`, it finds all non-abstract concrete derived types using the Roslyn symbol API (no runtime reflection). | ||
| 3. It generates a `[ModuleInitializer]` that registers those types in `TriggerTypeRegistry` before any app code runs. | ||
|
|
||
| ```csharp | ||
| // 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. | ||
|
|
||
| ### CommandRunner dispatcher | ||
|
|
||
| `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. | ||
|
|
||
| ## Multiple state machines sharing the same trigger type | ||
|
|
||
| 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: | ||
|
|
||
| ```csharp | ||
| // 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. | ||
|
|
||
| ## Related pages | ||
|
|
||
| - [Target Framework Compatibility](./Target-Framework-Compatibility.md) | ||
| - [Packages](./Packages.md) | ||
| - [Static Analysis](./Static-Analysis-for-State-Machine-Configuration.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Target Framework Compatibility | ||
|
|
||
| `FunctionalStateMachine.Core` and `FunctionalStateMachine.CommandRunner` both multi-target so the correct build is selected automatically based on your project's target framework. | ||
|
|
||
| ## Supported targets | ||
|
|
||
| | Target framework | Gets | | ||
| |---|---| | ||
| | `netstandard2.0` | Broadest compatibility — .NET Framework 4.6.1+, .NET Core 2.0+, Xamarin, Unity | | ||
| | `net8.0` | AOT-compatible build with full trim analysis | | ||
|
|
||
| When you reference the NuGet package from a `net8.0` or `net9.0` project, NuGet automatically selects the `net8.0` build. When you reference it from a `netstandard2.0`-compatible target, NuGet selects the `netstandard2.0` build. | ||
|
|
||
| ## Feature comparison | ||
|
|
||
| All state machine features are identical across both targets. The only difference is in the internals: | ||
|
|
||
| | Feature | `netstandard2.0` | `net8.0+` | | ||
| |---|---|---| | ||
| | Full fluent API | ✅ | ✅ | | ||
| | Guards, conditionals | ✅ | ✅ | | ||
| | Hierarchical states | ✅ | ✅ | | ||
| | Static analysis on `.Build()` | ✅ | ✅ | | ||
| | Unused-trigger analysis | ✅ (when generator active) | ✅ (when generator active) | | ||
| | AOT / NativeAOT safe | ⚠️ (N/A for `netstandard2.0`) | ✅ | | ||
| | Trim-safe | ⚠️ (N/A for `netstandard2.0`) | ✅ | | ||
| | `[ModuleInitializer]` for trigger registry | ✅ (when PolySharp present) | ✅ | | ||
|
|
||
| ## Source generator compatibility | ||
|
|
||
| The source generator (`FunctionalStateMachine.Core.Generator`) is a `netstandard2.0` Roslyn analyzer and works with all target frameworks. The generated `[ModuleInitializer]` code requires the `System.Runtime.CompilerServices.ModuleInitializerAttribute` type, which is: | ||
|
|
||
| - Available natively in `.NET 5` and later | ||
| - Back-ported to `netstandard2.0` by [PolySharp](https://github.com/Sergio0694/PolySharp) if you have it installed | ||
|
|
||
| If neither is available (e.g. plain `netstandard2.0` without PolySharp), the generator silently skips generation and unused-trigger analysis is bypassed without error. | ||
|
|
||
| ## Using with .NET Framework | ||
|
|
||
| If your project targets `.NET Framework`, the `netstandard2.0` build is used. The state machine works fully, but: | ||
|
|
||
| - Unused-trigger analysis will not report warnings (no `[ModuleInitializer]` support without PolySharp) | ||
| - AOT and trim tooling are not applicable | ||
|
|
||
| ## Related pages | ||
|
|
||
| - [AOT and Trim Compatibility](./AOT-and-Trim-Compatibility.md) | ||
| - [Packages](./Packages.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/FunctionalStateMachine.Core.Generator/FunctionalStateMachine.Core.Generator.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>netstandard2.0</TargetFramework> | ||
| <LangVersion>latest</LangVersion> | ||
| <Nullable>enable</Nullable> | ||
| <IsRoslynAnalyzer>true</IsRoslynAnalyzer> | ||
| <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||
| <NoWarn>$(NoWarn);RS1035</NoWarn> | ||
| <IsPackable>false</IsPackable> | ||
|
|
||
| <!-- This is a build-time-only analyzer; do not trim or publish it as a runtime dependency --> | ||
| <IsPublishable>false</IsPublishable> | ||
| <PublishTrimmed>false</PublishTrimmed> | ||
| <PublishAot>false</PublishAot> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" /> | ||
| <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot - how does someone consuming the package from Nuget do this? Does the consumer need to reference the core package and add the generator package as a reference or can we make referencing the main package sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referencing
FunctionalStateMachine.Corefrom NuGet is sufficient — no separate generator package needed. The generator DLL is bundled inside the Core NuGet package atanalyzers/dotnet/cs, which NuGet automatically applies as an analyzer.The explicit
Core.Generatorproject reference in the sample apps is only necessary because they use project references (not NuGet packages). That's the exact equivalent of what NuGet does automatically for package consumers.