Skip to content

Commit 398bfae

Browse files
authored
Add AOT Markup optimisations (#4092)
<!-- Please be sure to read the [Contribute](https://github.com/reactiveui/reactiveui#contribute) section of the README --> **What kind of change does this PR introduce?** <!-- Bug fix, feature, docs update, ... --> Update **What is the current behavior?** <!-- You can also link to an open issue here. --> AOT Markup is broadly applied **What is the new behavior?** <!-- If this is a feature change --> A more detailed approach has been applied targeting methods and constructors primarily followed by class level attributes where required. This pull request adds comprehensive support and validation for Native AOT (Ahead-of-Time) compilation in ReactiveUI, ensuring that the framework and its usage patterns work correctly in AOT scenarios. The changes include new AOT compatibility test suites, updates to package dependencies, build property enhancements for AOT, and detailed documentation of the current AOT implementation status and developer guidance. **Key highlights:** - New test projects comprehensively validate both basic and advanced ReactiveUI scenarios under AOT, using proper suppression attributes in test code (not production). - Build system updated to ensure AOT compatibility flags are set for supported frameworks. - Package dependencies updated to latest versions for improved compatibility. - Documentation added to communicate the status, guidance, and best practices for AOT usage in ReactiveUI. **Most important changes:** **AOT Test Coverage and Validation** - Added `AOTCompatibilityTests.cs` and `AdvancedAOTTests.cs` to the `ReactiveUI.AOTTests` project, providing extensive test coverage for ReactiveUI features under Native AOT, including property changes, commands, property helpers, routing, validation, activation, dependency resolution, and message bus scenarios. Tests use `UnconditionalSuppressMessage` where required, following best practices to keep production code clean. [[1]](diffhunk://#diff-a93b5fa329715d7daea578ebe28ce19997daf810660d8332e2fbeac6e237471bR1-R175) [[2]](diffhunk://#diff-611a62d85cb3379755de7bfd5aae97f12a4c9be3cb12d767ee563abe11a3cbecR1-R139) **Build and Project System Enhancements** - Updated `Directory.build.props` to set `<IsAotCompatible>true>` for all non-test projects targeting .NET 8.0 or 9.0, ensuring the build system recognizes and optimizes for AOT scenarios. **Dependency Updates for Compatibility** - Upgraded several package versions in `Directory.Packages.props` for improved compatibility and stability, including `Splat`, `System.Text.Json`, and `Microsoft.AspNetCore.Components`. Also added `Microsoft.Windows.CsWinRT` as a new dependency. [[1]](diffhunk://#diff-4d59c677ea4c9112e0ce2f2e527693f48dcbcf66ba4eeb4b01abb5f3da8bbe11L7-R7) [[2]](diffhunk://#diff-4d59c677ea4c9112e0ce2f2e527693f48dcbcf66ba4eeb4b01abb5f3da8bbe11L37-R37) [[3]](diffhunk://#diff-4d59c677ea4c9112e0ce2f2e527693f48dcbcf66ba4eeb4b01abb5f3da8bbe11R48) [[4]](diffhunk://#diff-4d59c677ea4c9112e0ce2f2e527693f48dcbcf66ba4eeb4b01abb5f3da8bbe11L59-R60) These changes collectively aim to ensure that ReactiveUI is fully ready for Native AOT compilation, with clear guidance and validation for developers targeting modern .NET platforms. **What might this PR break?** AOT Trimming may be affected **Please check if the PR fulfills these requirements** - [x] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) **Other information**:
1 parent 1e340e4 commit 398bfae

File tree

208 files changed

+4726
-723
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

208 files changed

+4726
-723
lines changed

src/Directory.Packages.props

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
55
</PropertyGroup>
66
<PropertyGroup>
7-
<SplatVersion>15.3.1</SplatVersion>
7+
<SplatVersion>15.4.1</SplatVersion>
88
<XamarinAndroidXCoreVersion>1.13.1.4</XamarinAndroidXCoreVersion>
99
<XamarinAndroidXLifecycleLiveDataVersion>2.8.4.1</XamarinAndroidXLifecycleLiveDataVersion>
1010
</PropertyGroup>
@@ -34,7 +34,7 @@
3434
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
3535
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
3636
<PackageVersion Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
37-
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
37+
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
3838
<PackageVersion Include="Verify.Xunit" Version="30.5.0" />
3939
<PackageVersion Include="xunit" Version="2.9.3" />
4040
<PackageVersion Include="xunit.runner.console" Version="2.9.3" />
@@ -45,6 +45,7 @@
4545
<PackageVersion Include="Xamarin.AndroidX.Legacy.Support.Core.UI" Version="1.0.0.29" />
4646
<PackageVersion Include="Xamarin.Google.Android.Material" Version="1.11.0.2" />
4747
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData" Version="$(XamarinAndroidXLifecycleLiveDataVersion)" />
48+
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
4849
</ItemGroup>
4950
<ItemGroup Condition="'$(UseMaui)' != 'true'">
5051
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
@@ -56,7 +57,7 @@
5657
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
5758
</ItemGroup>
5859
<ItemGroup Condition="$(TargetFramework.StartsWith('net9'))">
59-
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.7" />
60+
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.8" />
6061
<PackageVersion Include="Microsoft.Maui.Controls" Version="9.0.90" />
6162
<PackageVersion Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.90" />
6263
</ItemGroup>

src/Directory.build.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
<WindowsTargetFrameworks>net462;net472;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0</WindowsTargetFrameworks>
3636
<MobileTargetFrameworks>net8.0-android;net8.0-ios;net8.0-tvos;net8.0-macos;net8.0-maccatalyst;net9.0-android;net9.0-ios;net9.0-tvos;net9.0-macos;net9.0-maccatalyst</MobileTargetFrameworks>
3737
<BaseTargetFrameworks>netstandard2.0;net8.0;net9.0</BaseTargetFrameworks>
38+
</PropertyGroup>
39+
<PropertyGroup Condition="'$(IsTestProject)' != 'true' and ($(TargetFramework.StartsWith('net8.0')) or $(TargetFramework.StartsWith('net9.0')))">
40+
<IsAotCompatible>true</IsAotCompatible>
3841
</PropertyGroup>
3942
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
4043
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Reactive;
8+
using System.Reactive.Linq;
9+
using ReactiveUI;
10+
using Xunit;
11+
12+
namespace ReactiveUI.AOTTests;
13+
14+
/// <summary>
15+
/// Tests to verify that ReactiveUI works correctly in AOT (Ahead-of-Time) compilation scenarios.
16+
/// These tests ensure that the library doesn't rely on reflection in ways that break with AOT.
17+
/// </summary>
18+
public class AOTCompatibilityTests
19+
{
20+
/// <summary>
21+
/// Tests that ReactiveObjects can be created and property changes work in AOT.
22+
/// </summary>
23+
[Fact]
24+
public void ReactiveObject_PropertyChanges_WorksInAOT()
25+
{
26+
var obj = new TestReactiveObject();
27+
var propertyChanged = false;
28+
29+
obj.PropertyChanged += (s, e) => propertyChanged = true;
30+
obj.TestProperty = "New Value";
31+
32+
Assert.True(propertyChanged);
33+
Assert.Equal("New Value", obj.TestProperty);
34+
}
35+
36+
/// <summary>
37+
/// Tests that ReactiveCommands can be created and executed in AOT.
38+
/// </summary>
39+
[Fact]
40+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")]
41+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")]
42+
public void ReactiveCommand_Create_WorksInAOT()
43+
{
44+
var executed = false;
45+
var command = ReactiveCommand.Create(() => executed = true);
46+
47+
command.Execute().Subscribe();
48+
49+
Assert.True(executed);
50+
}
51+
52+
/// <summary>
53+
/// Tests that ReactiveCommands with parameters work in AOT.
54+
/// </summary>
55+
[Fact]
56+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")]
57+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")]
58+
public void ReactiveCommand_CreateWithParameter_WorksInAOT()
59+
{
60+
string? result = null;
61+
var command = ReactiveCommand.Create<string>(param => result = param);
62+
63+
command.Execute("test").Subscribe();
64+
65+
Assert.Equal("test", result);
66+
}
67+
68+
/// <summary>
69+
/// Tests that ObservableAsPropertyHelper works in AOT.
70+
/// </summary>
71+
[Fact]
72+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")]
73+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")]
74+
public void ObservableAsPropertyHelper_WorksInAOT()
75+
{
76+
var obj = new TestReactiveObject();
77+
78+
// Test string-based property helper (should work in AOT)
79+
var helper = Observable.Return("computed value")
80+
.ToProperty(obj, nameof(TestReactiveObject.ComputedProperty));
81+
82+
Assert.Equal("computed value", helper.Value);
83+
}
84+
85+
/// <summary>
86+
/// Tests that WhenAnyValue works with string property names in AOT.
87+
/// </summary>
88+
[Fact]
89+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing WhenAnyValue which requires AOT suppression")]
90+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing WhenAnyValue which requires AOT suppression")]
91+
public void WhenAnyValue_StringPropertyNames_WorksInAOT()
92+
{
93+
var obj = new TestReactiveObject();
94+
string? observedValue = null;
95+
96+
// Using string property names should work in AOT
97+
obj.WhenAnyValue(x => x.TestProperty)
98+
.Subscribe(value => observedValue = value);
99+
100+
obj.TestProperty = "test value";
101+
102+
Assert.Equal("test value", observedValue);
103+
}
104+
105+
/// <summary>
106+
/// Tests that interaction requests work in AOT.
107+
/// </summary>
108+
[Fact]
109+
public void Interaction_WorksInAOT()
110+
{
111+
var interaction = new Interaction<string, bool>();
112+
var called = false;
113+
114+
interaction.RegisterHandler(context =>
115+
{
116+
called = true;
117+
context.SetOutput(true);
118+
});
119+
120+
var result = interaction.Handle("test").Wait();
121+
122+
Assert.True(called);
123+
Assert.True(result);
124+
}
125+
126+
/// <summary>
127+
/// Tests that INPC property observation works in AOT.
128+
/// </summary>
129+
[Fact]
130+
public void INPCPropertyObservation_WorksInAOT()
131+
{
132+
var obj = new TestReactiveObject();
133+
var changes = new List<string?>();
134+
135+
obj.PropertyChanged += (s, e) => changes.Add(e.PropertyName);
136+
137+
obj.TestProperty = "value1";
138+
obj.TestProperty = "value2";
139+
140+
Assert.Contains(nameof(TestReactiveObject.TestProperty), changes);
141+
Assert.Equal(2, changes.Count(x => x == nameof(TestReactiveObject.TestProperty)));
142+
}
143+
144+
/// <summary>
145+
/// Tests that ReactiveCommand.CreateFromObservable works in AOT scenarios.
146+
/// </summary>
147+
[Fact]
148+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible ReactiveCommand.CreateFromObservable method")]
149+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.CreateFromObservable method")]
150+
public void ReactiveCommand_CreateFromObservable_WorksInAOT()
151+
{
152+
var result = 0;
153+
var command = ReactiveCommand.CreateFromObservable(() => Observable.Return(42));
154+
155+
command.Subscribe(x => result = x);
156+
command.Execute().Subscribe();
157+
158+
Assert.Equal(42, result);
159+
}
160+
161+
/// <summary>
162+
/// Tests that string-based property bindings work in AOT (preferred pattern).
163+
/// </summary>
164+
[Fact]
165+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")]
166+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")]
167+
public void StringBasedPropertyBinding_WorksInAOT()
168+
{
169+
var obj = new TestReactiveObject();
170+
var helper = Observable.Return("test")
171+
.ToProperty(obj, nameof(TestReactiveObject.ComputedProperty));
172+
173+
Assert.Equal("test", helper.Value);
174+
}
175+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Reactive;
8+
using System.Reactive.Disposables;
9+
using System.Reactive.Linq;
10+
using System.Reactive.Subjects;
11+
using ReactiveUI;
12+
using Splat;
13+
using Xunit;
14+
15+
namespace ReactiveUI.AOTTests;
16+
17+
/// <summary>
18+
/// Additional AOT compatibility tests for more advanced scenarios.
19+
/// </summary>
20+
public class AdvancedAOTTests
21+
{
22+
/// <summary>
23+
/// Tests that routing functionality works in AOT.
24+
/// </summary>
25+
[Fact]
26+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible RoutingState which uses ReactiveCommand")]
27+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible RoutingState which uses ReactiveCommand")]
28+
public void RoutingState_Navigation_WorksInAOT()
29+
{
30+
var routingState = new RoutingState();
31+
var viewModel = new TestRoutableViewModel();
32+
33+
// Test navigation
34+
routingState.Navigate.Execute(viewModel).Subscribe();
35+
36+
Assert.Single(routingState.NavigationStack);
37+
Assert.Equal(viewModel, routingState.NavigationStack[0]);
38+
}
39+
40+
/// <summary>
41+
/// Tests that property validation works in AOT scenarios.
42+
/// </summary>
43+
[Fact]
44+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty constructor that uses RxApp")]
45+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")]
46+
public void PropertyValidation_WorksInAOT()
47+
{
48+
var property = new ReactiveProperty<string>(string.Empty);
49+
var hasErrors = false;
50+
51+
property.ObserveValidationErrors()
52+
.Subscribe(error => hasErrors = !string.IsNullOrEmpty(error));
53+
54+
property.AddValidationError(x => string.IsNullOrEmpty(x) ? "Required" : null);
55+
property.Value = string.Empty;
56+
57+
Assert.True(hasErrors);
58+
}
59+
60+
/// <summary>
61+
/// Tests that view model activation works in AOT.
62+
/// </summary>
63+
[Fact]
64+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty constructor that uses RxApp")]
65+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")]
66+
public void ViewModelActivation_WorksInAOT()
67+
{
68+
var viewModel = new TestActivatableViewModel();
69+
var activated = false;
70+
var deactivated = false;
71+
72+
viewModel.WhenActivated(disposables =>
73+
{
74+
activated = true;
75+
Disposable.Create(() => deactivated = true).DisposeWith(disposables);
76+
});
77+
78+
viewModel.Activator.Activate();
79+
Assert.True(activated);
80+
81+
viewModel.Activator.Deactivate();
82+
Assert.True(deactivated);
83+
}
84+
85+
/// <summary>
86+
/// Tests that observable property helpers work correctly in AOT.
87+
/// </summary>
88+
[Fact]
89+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty which requires AOT suppression")]
90+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty which requires AOT suppression")]
91+
public void ObservableAsPropertyHelper_Lifecycle_WorksInAOT()
92+
{
93+
var testObject = new TestReactiveObject();
94+
var source = new BehaviorSubject<string>("initial");
95+
96+
var helper = source.ToProperty(testObject, nameof(TestReactiveObject.ComputedProperty));
97+
98+
Assert.Equal("initial", helper.Value);
99+
100+
source.OnNext("updated");
101+
Assert.Equal("updated", helper.Value);
102+
103+
source.OnCompleted();
104+
helper.Dispose();
105+
}
106+
107+
/// <summary>
108+
/// Tests that dependency resolution works in AOT.
109+
/// </summary>
110+
[Fact]
111+
public void DependencyResolution_BasicOperations_WorkInAOT()
112+
{
113+
var resolver = Locator.CurrentMutable;
114+
115+
// Test basic registration and resolution
116+
resolver.RegisterConstant<string>("test value");
117+
var resolved = Locator.Current.GetService<string>();
118+
119+
Assert.Equal("test value", resolved);
120+
}
121+
122+
/// <summary>
123+
/// Tests that message bus functionality works in AOT.
124+
/// </summary>
125+
[Fact]
126+
public void MessageBus_Operations_WorkInAOT()
127+
{
128+
var messageBus = new MessageBus();
129+
var received = false;
130+
var testMessage = "test message";
131+
132+
messageBus.Listen<string>().Subscribe(msg =>
133+
{
134+
received = msg == testMessage;
135+
});
136+
137+
messageBus.SendMessage(testMessage);
138+
139+
Assert.True(received);
140+
}
141+
}

0 commit comments

Comments
 (0)