Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<PropertyGroup>
<SplatVersion>15.3.1</SplatVersion>
<SplatVersion>15.4.1</SplatVersion>
<XamarinAndroidXCoreVersion>1.13.1.4</XamarinAndroidXCoreVersion>
<XamarinAndroidXLifecycleLiveDataVersion>2.8.4.1</XamarinAndroidXLifecycleLiveDataVersion>
</PropertyGroup>
Expand Down Expand Up @@ -34,7 +34,7 @@
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="Verify.Xunit" Version="30.5.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.console" Version="2.9.3" />
Expand All @@ -45,6 +45,7 @@
<PackageVersion Include="Xamarin.AndroidX.Legacy.Support.Core.UI" Version="1.0.0.29" />
<PackageVersion Include="Xamarin.Google.Android.Material" Version="1.11.0.2" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData" Version="$(XamarinAndroidXLifecycleLiveDataVersion)" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
</ItemGroup>
<ItemGroup Condition="'$(UseMaui)' != 'true'">
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
Expand All @@ -56,7 +57,7 @@
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net9'))">
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.8" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="9.0.90" />
<PackageVersion Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.90" />
</ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/Directory.build.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
<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>
<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>
<BaseTargetFrameworks>netstandard2.0;net8.0;net9.0</BaseTargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(IsTestProject)' != 'true' and ($(TargetFramework.StartsWith('net8.0')) or $(TargetFramework.StartsWith('net9.0')))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
Expand Down
175 changes: 175 additions & 0 deletions src/ReactiveUI.AOTTests/AOTCompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using Xunit;

namespace ReactiveUI.AOTTests;

/// <summary>
/// Tests to verify that ReactiveUI works correctly in AOT (Ahead-of-Time) compilation scenarios.
/// These tests ensure that the library doesn't rely on reflection in ways that break with AOT.
/// </summary>
public class AOTCompatibilityTests
{
/// <summary>
/// Tests that ReactiveObjects can be created and property changes work in AOT.
/// </summary>
[Fact]
public void ReactiveObject_PropertyChanges_WorksInAOT()
{
var obj = new TestReactiveObject();
var propertyChanged = false;

obj.PropertyChanged += (s, e) => propertyChanged = true;
obj.TestProperty = "New Value";

Assert.True(propertyChanged);
Assert.Equal("New Value", obj.TestProperty);
}

/// <summary>
/// Tests that ReactiveCommands can be created and executed in AOT.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")]
public void ReactiveCommand_Create_WorksInAOT()
{
var executed = false;
var command = ReactiveCommand.Create(() => executed = true);

command.Execute().Subscribe();

Assert.True(executed);
}

/// <summary>
/// Tests that ReactiveCommands with parameters work in AOT.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")]
public void ReactiveCommand_CreateWithParameter_WorksInAOT()
{
string? result = null;
var command = ReactiveCommand.Create<string>(param => result = param);

command.Execute("test").Subscribe();

Assert.Equal("test", result);
}

/// <summary>
/// Tests that ObservableAsPropertyHelper works in AOT.
/// </summary>
[Fact]
[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")]
[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")]
public void ObservableAsPropertyHelper_WorksInAOT()
{
var obj = new TestReactiveObject();

// Test string-based property helper (should work in AOT)
var helper = Observable.Return("computed value")
.ToProperty(obj, nameof(TestReactiveObject.ComputedProperty));

Assert.Equal("computed value", helper.Value);
}

/// <summary>
/// Tests that WhenAnyValue works with string property names in AOT.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing WhenAnyValue which requires AOT suppression")]
public void WhenAnyValue_StringPropertyNames_WorksInAOT()
{
var obj = new TestReactiveObject();
string? observedValue = null;

// Using string property names should work in AOT
obj.WhenAnyValue(x => x.TestProperty)
.Subscribe(value => observedValue = value);

obj.TestProperty = "test value";

Assert.Equal("test value", observedValue);
}

/// <summary>
/// Tests that interaction requests work in AOT.
/// </summary>
[Fact]
public void Interaction_WorksInAOT()
{
var interaction = new Interaction<string, bool>();
var called = false;

interaction.RegisterHandler(context =>
{
called = true;
context.SetOutput(true);
});

var result = interaction.Handle("test").Wait();

Assert.True(called);
Assert.True(result);
}

/// <summary>
/// Tests that INPC property observation works in AOT.
/// </summary>
[Fact]
public void INPCPropertyObservation_WorksInAOT()
{
var obj = new TestReactiveObject();
var changes = new List<string?>();

obj.PropertyChanged += (s, e) => changes.Add(e.PropertyName);

obj.TestProperty = "value1";
obj.TestProperty = "value2";

Assert.Contains(nameof(TestReactiveObject.TestProperty), changes);
Assert.Equal(2, changes.Count(x => x == nameof(TestReactiveObject.TestProperty)));
}

/// <summary>
/// Tests that ReactiveCommand.CreateFromObservable works in AOT scenarios.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.CreateFromObservable method")]
public void ReactiveCommand_CreateFromObservable_WorksInAOT()
{
var result = 0;
var command = ReactiveCommand.CreateFromObservable(() => Observable.Return(42));

command.Subscribe(x => result = x);
command.Execute().Subscribe();

Assert.Equal(42, result);
}

/// <summary>
/// Tests that string-based property bindings work in AOT (preferred pattern).
/// </summary>
[Fact]
[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")]
[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")]
public void StringBasedPropertyBinding_WorksInAOT()
{
var obj = new TestReactiveObject();
var helper = Observable.Return("test")
.ToProperty(obj, nameof(TestReactiveObject.ComputedProperty));

Assert.Equal("test", helper.Value);
}
}
141 changes: 141 additions & 0 deletions src/ReactiveUI.AOTTests/AdvancedAOTTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using ReactiveUI;
using Splat;
using Xunit;

namespace ReactiveUI.AOTTests;

/// <summary>
/// Additional AOT compatibility tests for more advanced scenarios.
/// </summary>
public class AdvancedAOTTests
{
/// <summary>
/// Tests that routing functionality works in AOT.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible RoutingState which uses ReactiveCommand")]
public void RoutingState_Navigation_WorksInAOT()
{
var routingState = new RoutingState();
var viewModel = new TestRoutableViewModel();

// Test navigation
routingState.Navigate.Execute(viewModel).Subscribe();

Assert.Single(routingState.NavigationStack);
Assert.Equal(viewModel, routingState.NavigationStack[0]);
}

/// <summary>
/// Tests that property validation works in AOT scenarios.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")]
public void PropertyValidation_WorksInAOT()
{
var property = new ReactiveProperty<string>(string.Empty);
var hasErrors = false;

property.ObserveValidationErrors()
.Subscribe(error => hasErrors = !string.IsNullOrEmpty(error));

property.AddValidationError(x => string.IsNullOrEmpty(x) ? "Required" : null);
property.Value = string.Empty;

Assert.True(hasErrors);
}

/// <summary>
/// Tests that view model activation works in AOT.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")]
public void ViewModelActivation_WorksInAOT()
{
var viewModel = new TestActivatableViewModel();
var activated = false;
var deactivated = false;

viewModel.WhenActivated(disposables =>
{
activated = true;
Disposable.Create(() => deactivated = true).DisposeWith(disposables);
});

viewModel.Activator.Activate();
Assert.True(activated);

viewModel.Activator.Deactivate();
Assert.True(deactivated);
}

/// <summary>
/// Tests that observable property helpers work correctly in AOT.
/// </summary>
[Fact]
[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")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty which requires AOT suppression")]
public void ObservableAsPropertyHelper_Lifecycle_WorksInAOT()
{
var testObject = new TestReactiveObject();
var source = new BehaviorSubject<string>("initial");

var helper = source.ToProperty(testObject, nameof(TestReactiveObject.ComputedProperty));

Assert.Equal("initial", helper.Value);

source.OnNext("updated");
Assert.Equal("updated", helper.Value);

source.OnCompleted();
helper.Dispose();
}

/// <summary>
/// Tests that dependency resolution works in AOT.
/// </summary>
[Fact]
public void DependencyResolution_BasicOperations_WorkInAOT()
{
var resolver = Locator.CurrentMutable;

// Test basic registration and resolution
resolver.RegisterConstant<string>("test value");
var resolved = Locator.Current.GetService<string>();

Assert.Equal("test value", resolved);
}

/// <summary>
/// Tests that message bus functionality works in AOT.
/// </summary>
[Fact]
public void MessageBus_Operations_WorkInAOT()
{
var messageBus = new MessageBus();
var received = false;
var testMessage = "test message";

messageBus.Listen<string>().Subscribe(msg =>
{
received = msg == testMessage;
});

messageBus.SendMessage(testMessage);

Assert.True(received);
}
}
Loading
Loading