Skip to content

Commit cebe914

Browse files
authored
Merge pull request #28 from leeoades/copilot/make-libraries-aot-and-trimmed
AOT/trim compatibility: source-generated trigger registry, sample publishing, multi-machine tests, and compatibility docs
2 parents cf7e6fa + 43ffdf0 commit cebe914

File tree

18 files changed

+842
-59
lines changed

18 files changed

+842
-59
lines changed

FunctionalStateMachine.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalStateMachine.Benc
6565
EndProject
6666
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
6767
EndProject
68+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalStateMachine.Core.Generator", "src\FunctionalStateMachine.Core.Generator\FunctionalStateMachine.Core.Generator.csproj", "{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}"
69+
EndProject
6870
Global
6971
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7072
Debug|Any CPU = Debug|Any CPU
@@ -231,6 +233,18 @@ Global
231233
{C2FBB31B-EE96-4ECB-B077-6D9313DB1555}.Release|x64.Build.0 = Release|Any CPU
232234
{C2FBB31B-EE96-4ECB-B077-6D9313DB1555}.Release|x86.ActiveCfg = Release|Any CPU
233235
{C2FBB31B-EE96-4ECB-B077-6D9313DB1555}.Release|x86.Build.0 = Release|Any CPU
236+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
237+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
238+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Debug|x64.ActiveCfg = Debug|Any CPU
239+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Debug|x64.Build.0 = Debug|Any CPU
240+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Debug|x86.ActiveCfg = Debug|Any CPU
241+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Debug|x86.Build.0 = Debug|Any CPU
242+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
243+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Release|Any CPU.Build.0 = Release|Any CPU
244+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Release|x64.ActiveCfg = Release|Any CPU
245+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Release|x64.Build.0 = Release|Any CPU
246+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Release|x86.ActiveCfg = Release|Any CPU
247+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4}.Release|x86.Build.0 = Release|Any CPU
234248
EndGlobalSection
235249
GlobalSection(SolutionProperties) = preSolution
236250
HideSolutionNode = FALSE
@@ -251,5 +265,6 @@ Global
251265
{D1FA70BE-F53F-492A-83E3-A1D34267795E} = {A96AD5CA-C4AA-45C6-A86D-69F8ED944894}
252266
{E5866FE3-15DF-4362-95A6-C6F651434249} = {A96AD5CA-C4AA-45C6-A86D-69F8ED944894}
253267
{C2FBB31B-EE96-4ECB-B077-6D9313DB1555} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
268+
{FE472548-CAE7-4E2A-A6B5-0DDC70D7FCD4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
254269
EndGlobalSection
255270
EndGlobal

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,15 @@ Explore complete, runnable examples in the `/samples` directory:
485485

486486
---
487487

488+
## Advanced Topics
489+
490+
This library targets both `netstandard2.0` (broad compatibility) and `net8.0` (AOT/trim-ready). Both NativeAOT and `PublishTrimmed` publishing are supported with zero runtime reflection.
491+
492+
- **[Target framework compatibility](docs/Target-Framework-Compatibility.md)** — .NET Standard 2.0 vs .NET 8+, what each target provides
493+
- **[AOT and trim compatibility](docs/AOT-and-Trim-Compatibility.md)** — NativeAOT, `PublishTrimmed`, how source generators eliminate reflection
494+
495+
---
496+
488497
## Installation
489498

490499
```bash

docs/AOT-and-Trim-Compatibility.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# AOT and Trim Compatibility
2+
3+
The `net8.0` build of `FunctionalStateMachine.Core` and `FunctionalStateMachine.CommandRunner` is fully compatible with:
4+
5+
- **NativeAOT** (`PublishAot=true`) — compiled ahead of time to a self-contained native binary
6+
- **Trimming** (`PublishTrimmed=true`) — unused code removed at publish time to reduce binary size
7+
- **Single-file publishing** (`PublishSingleFile=true`)
8+
9+
## Status
10+
11+
| Package | `IsAotCompatible` | Reflection-free | Trim-safe |
12+
|---|---|---|---|
13+
| `FunctionalStateMachine.Core` | ✅ (`net8.0+`) |||
14+
| `FunctionalStateMachine.CommandRunner` | ✅ (`net8.0+`) |||
15+
| `FunctionalStateMachine.Diagrams` | N/A (build-time only) | N/A | N/A |
16+
| `FunctionalStateMachine.Core.Generator` | N/A (build-time only) | N/A | N/A |
17+
| `FunctionalStateMachine.CommandRunner.Generator` | N/A (build-time only) | N/A | N/A |
18+
19+
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.
20+
21+
## Enabling publishing with trimming
22+
23+
### Executable projects
24+
25+
Add `<PublishTrimmed>true</PublishTrimmed>` to your `.csproj`:
26+
27+
```xml
28+
<Project Sdk="Microsoft.NET.Sdk">
29+
<PropertyGroup>
30+
<OutputType>Exe</OutputType>
31+
<TargetFramework>net9.0</TargetFramework>
32+
<PublishTrimmed>true</PublishTrimmed>
33+
</PropertyGroup>
34+
</Project>
35+
```
36+
37+
The source generator is bundled inside the `FunctionalStateMachine.Core` NuGet package and applied automatically — no extra package reference needed.
38+
39+
Then publish:
40+
41+
```bash
42+
dotnet publish -c Release -r linux-x64 --sc true
43+
```
44+
45+
### NativeAOT
46+
47+
```xml
48+
<PropertyGroup>
49+
<PublishAot>true</PublishAot>
50+
</PropertyGroup>
51+
```
52+
53+
```bash
54+
dotnet publish -c Release -r linux-x64
55+
```
56+
57+
## How it works: no reflection at runtime
58+
59+
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).
60+
61+
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.
62+
63+
### The source generator approach
64+
65+
The `FunctionalStateMachine.Core.Generator` Roslyn source generator solves this at compile time:
66+
67+
1. It detects all `StateMachine<…, TTrigger, …>.Create()` call sites in your compilation.
68+
2. For each unique `TTrigger`, it finds all non-abstract concrete derived types using the Roslyn symbol API (no runtime reflection).
69+
3. It generates a `[ModuleInitializer]` that registers those types in `TriggerTypeRegistry` before any app code runs.
70+
71+
```csharp
72+
// Example of generated output (in your bin/obj folder):
73+
// <auto-generated />
74+
[ModuleInitializer]
75+
internal static void Initialize()
76+
{
77+
TriggerTypeRegistry.Register<global::MyApp.OrderTrigger>(new[]
78+
{
79+
typeof(global::MyApp.OrderTrigger.Process),
80+
typeof(global::MyApp.OrderTrigger.Cancel),
81+
typeof(global::MyApp.OrderTrigger.Complete),
82+
});
83+
}
84+
```
85+
86+
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.
87+
88+
### CommandRunner dispatcher
89+
90+
`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.
91+
92+
## Multiple state machines sharing the same trigger type
93+
94+
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:
95+
96+
```csharp
97+
// Both machines share OrderTrigger; generator registers its types once.
98+
// machineA may use all triggers; machineB may only use a subset.
99+
// Each machine independently warns about its own unused triggers.
100+
101+
var machineA = StateMachine<StateA, OrderTrigger, DataA, CmdA>.Create()
102+
.For(StateA.Idle)
103+
.On<OrderTrigger.Process>() // uses Process
104+
.TransitionTo(StateA.Done)
105+
// ⚠️ warning: Cancel and Complete unused in this machine
106+
.Build();
107+
108+
var machineB = StateMachine<StateB, OrderTrigger, DataB, CmdB>.Create()
109+
.For(StateB.Idle)
110+
.On<OrderTrigger.Cancel>() // uses Cancel
111+
.TransitionTo(StateB.Cancelled)
112+
// ⚠️ warning: Process and Complete unused in this machine
113+
.Build();
114+
```
115+
116+
Both machines build successfully — unused-trigger warnings are informational and never block construction.
117+
118+
## Related pages
119+
120+
- [Target Framework Compatibility](./Target-Framework-Compatibility.md)
121+
- [Packages](./Packages.md)
122+
- [Static Analysis](./Static-Analysis-for-State-Machine-Configuration.md)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Target Framework Compatibility
2+
3+
`FunctionalStateMachine.Core` and `FunctionalStateMachine.CommandRunner` both multi-target so the correct build is selected automatically based on your project's target framework.
4+
5+
## Supported targets
6+
7+
| Target framework | Gets |
8+
|---|---|
9+
| `netstandard2.0` | Broadest compatibility — .NET Framework 4.6.1+, .NET Core 2.0+, Xamarin, Unity |
10+
| `net8.0` | AOT-compatible build with full trim analysis |
11+
12+
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.
13+
14+
## Feature comparison
15+
16+
All state machine features are identical across both targets. The only difference is in the internals:
17+
18+
| Feature | `netstandard2.0` | `net8.0+` |
19+
|---|---|---|
20+
| Full fluent API |||
21+
| Guards, conditionals |||
22+
| Hierarchical states |||
23+
| Static analysis on `.Build()` |||
24+
| Unused-trigger analysis | ✅ (when generator active) | ✅ (when generator active) |
25+
| AOT / NativeAOT safe | ⚠️ (N/A for `netstandard2.0`) ||
26+
| Trim-safe | ⚠️ (N/A for `netstandard2.0`) ||
27+
| `[ModuleInitializer]` for trigger registry | ✅ (when PolySharp present) ||
28+
29+
## Source generator compatibility
30+
31+
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:
32+
33+
- Available natively in `.NET 5` and later
34+
- Back-ported to `netstandard2.0` by [PolySharp](https://github.com/Sergio0694/PolySharp) if you have it installed
35+
36+
If neither is available (e.g. plain `netstandard2.0` without PolySharp), the generator silently skips generation and unused-trigger analysis is bypassed without error.
37+
38+
## Using with .NET Framework
39+
40+
If your project targets `.NET Framework`, the `netstandard2.0` build is used. The state machine works fully, but:
41+
42+
- Unused-trigger analysis will not report warnings (no `[ModuleInitializer]` support without PolySharp)
43+
- AOT and trim tooling are not applicable
44+
45+
## Related pages
46+
47+
- [AOT and Trim Compatibility](./AOT-and-Trim-Compatibility.md)
48+
- [Packages](./Packages.md)

docs/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ Welcome to the Functional State Machine docs. Each guide introduces a feature, w
3131
- [Mermaid diagram generation](Mermaid-Diagram-Generation.md)
3232
- [Command runners](Command-Runners.md)
3333

34+
## Advanced
35+
36+
- [Target framework compatibility (.NET Standard 2.0 vs .NET 8+)](Target-Framework-Compatibility.md)
37+
- [AOT and trim compatibility](AOT-and-Trim-Compatibility.md)
38+
3439
## For Contributors
3540

3641
- [AI documentation maintenance guide](AI-Documentation-Maintenance.md) - How to keep AI docs up-to-date

samples/Basic/FunctionalStateMachine.Samples/FunctionalStateMachine.Samples.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
<ItemGroup>
2222
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Core\FunctionalStateMachine.Core.csproj" />
23+
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Core.Generator\FunctionalStateMachine.Core.Generator.csproj"
24+
OutputItemType="Analyzer"
25+
ReferenceOutputAssembly="false" />
2326
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Diagrams\FunctionalStateMachine.Diagrams.csproj"
2427
OutputItemType="Analyzer"
2528
ReferenceOutputAssembly="false" />

samples/StockPurchaser/StockPurchaserSampleApp/StockPurchaserSampleApp.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
<TargetFramework>net9.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8+
<PublishTrimmed>true</PublishTrimmed>
89
</PropertyGroup>
910

1011
<ItemGroup>
1112
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Core\FunctionalStateMachine.Core.csproj" />
13+
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Core.Generator\FunctionalStateMachine.Core.Generator.csproj"
14+
OutputItemType="Analyzer"
15+
ReferenceOutputAssembly="false" />
1216
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Diagrams\FunctionalStateMachine.Diagrams.csproj"
1317
OutputItemType="Analyzer"
1418
ReferenceOutputAssembly="false" />

samples/VendingMachine/VendingMachineSampleApp/VendingMachineSampleApp.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<TargetFramework>net9.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8+
<PublishTrimmed>true</PublishTrimmed>
89
</PropertyGroup>
910

1011
<ItemGroup>
@@ -17,6 +18,9 @@
1718
ReferenceOutputAssembly="false" />
1819
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.CommandRunner\FunctionalStateMachine.CommandRunner.csproj" />
1920
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Core\FunctionalStateMachine.Core.csproj" />
21+
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Core.Generator\FunctionalStateMachine.Core.Generator.csproj"
22+
OutputItemType="Analyzer"
23+
ReferenceOutputAssembly="false" />
2024
<ProjectReference Include="..\..\..\src\FunctionalStateMachine.Diagrams\FunctionalStateMachine.Diagrams.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2125
</ItemGroup>
2226

src/FunctionalStateMachine.CommandRunner.Generator/FunctionalStateMachine.CommandRunner.Generator.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
99
<NoWarn>$(NoWarn);RS1035</NoWarn>
1010
<IsPackable>false</IsPackable>
11+
12+
<!-- This is a build-time-only analyzer; do not trim or publish it as a runtime dependency -->
13+
<IsPublishable>false</IsPublishable>
14+
<PublishTrimmed>false</PublishTrimmed>
15+
<PublishAot>false</PublishAot>
1116
</PropertyGroup>
1217

1318
<ItemGroup>

src/FunctionalStateMachine.CommandRunner/FunctionalStateMachine.CommandRunner.csproj

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netstandard2.0</TargetFramework>
4+
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
55
<LangVersion>latest</LangVersion>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
@@ -12,11 +12,20 @@
1212
<!-- XML Documentation -->
1313
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1414
<NoWarn>$(NoWarn);CS1591</NoWarn>
15+
16+
<!-- AOT and trimming compatibility (net8.0+) -->
17+
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
18+
<!-- Prevent trim/AOT flags from being applied to the netstandard2.0 build by consuming apps -->
19+
<PublishTrimmed Condition="'$(TargetFramework)' == 'netstandard2.0'">false</PublishTrimmed>
20+
<PublishAot Condition="'$(TargetFramework)' == 'netstandard2.0'">false</PublishAot>
1521
</PropertyGroup>
1622

1723
<!-- Polyfills for C# 9+ features on netstandard2.0 -->
18-
<ItemGroup>
24+
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
1925
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="all" />
26+
</ItemGroup>
27+
28+
<ItemGroup>
2029
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
2130
</ItemGroup>
2231

0 commit comments

Comments
 (0)