diff --git a/README.md b/README.md index e6c8cb183d..f563d67034 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ [🔨 Get Started](https://reactiveui.net/docs/getting-started/) [🛍 Install Packages](https://reactiveui.net/docs/getting-started/installation/) [🎞 Watch Videos](https://reactiveui.net/docs/resources/videos) [🎓 View Samples](https://reactiveui.net/docs/resources/samples/) [🎤 Discuss ReactiveUI](https://reactiveui.net/slack) +## Documentation + +- [RxSchedulers](docs/RxSchedulers.md) - Using ReactiveUI schedulers without RequiresUnreferencedCode attributes + ## Book There has been an excellent [book](https://kent-boogaart.com/you-i-and-reactiveui/) written by our Alumni maintainer Kent Boogart. diff --git a/docs/RxSchedulers.md b/docs/RxSchedulers.md new file mode 100644 index 0000000000..d424ad9e48 --- /dev/null +++ b/docs/RxSchedulers.md @@ -0,0 +1,131 @@ +# RxSchedulers: Consuming ReactiveUI Schedulers Without RequiresUnreferencedCode + +## Problem + +When using `RxApp.MainThreadScheduler` or `RxApp.TaskpoolScheduler` in your code, since the entire `RxApp` class triggers initialization that is marked with `RequiresUnreferencedCode` attributes, any code that consumes these schedulers must also be marked with the same attributes. + +This is particularly problematic when creating observables in ViewModels, Repositories, or other deeper code that is consumed by multiple sources, as it forces all consumers to add `RequiresUnreferencedCode` attributes. + +## Solution + +The new `RxSchedulers` static class provides access to the same scheduler functionality without requiring unreferenced code attributes. This class contains only the scheduler properties and doesn't trigger the Splat dependency injection initialization that requires reflection. + +## Usage Examples + +### Basic Usage + +```csharp +// Old way - requires RequiresUnreferencedCode attribute +[RequiresUnreferencedCode("Uses RxApp which may require unreferenced code")] +public IObservable GetDataOld() +{ + return Observable.Return("data") + .ObserveOn(RxApp.MainThreadScheduler); // Triggers RequiresUnreferencedCode +} + +// New way - no attributes required +public IObservable GetDataNew() +{ + return Observable.Return("data") + .ObserveOn(RxSchedulers.MainThreadScheduler); // No attributes needed! +} +``` + +### ViewModel Example + +```csharp +public class MyViewModel : ReactiveObject +{ + private readonly ObservableAsPropertyHelper _greeting; + + public MyViewModel() + { + // Using RxSchedulers avoids RequiresUnreferencedCode + _greeting = this.WhenAnyValue(x => x.Name) + .Select(name => $"Hello, {name ?? "World"}!") + .ObserveOn(RxSchedulers.MainThreadScheduler) // No attributes needed! + .ToProperty(this, nameof(Greeting), scheduler: RxSchedulers.MainThreadScheduler); + } + + public string? Name { get; set; } + public string Greeting => _greeting.Value; +} +``` + +### Repository Pattern + +```csharp +public class DataRepository +{ + public IObservable GetProcessedData() + { + // Using RxSchedulers in repository code doesn't force consumers + // to add RequiresUnreferencedCode attributes + return GetRawData() + .ObserveOn(RxSchedulers.TaskpoolScheduler) // Background processing + .Select(ProcessData) + .ObserveOn(RxSchedulers.MainThreadScheduler); // UI updates + } +} +``` + +### ReactiveProperty Factory Methods + +```csharp +// New factory methods that use RxSchedulers internally +var property1 = ReactiveProperty.Create(); // No attributes required +var property2 = ReactiveProperty.Create("initial value"); +var property3 = ReactiveProperty.Create(42, skipCurrentValueOnSubscribe: false, allowDuplicateValues: true); +``` + +## API Reference + +### RxSchedulers Properties + +- `RxSchedulers.MainThreadScheduler` - Scheduler for UI thread operations (no unit test detection) +- `RxSchedulers.TaskpoolScheduler` - Scheduler for background operations (no unit test detection) + +### ReactiveProperty Factory Methods + +- `ReactiveProperty.Create()` - Creates with default scheduler +- `ReactiveProperty.Create(T initialValue)` - Creates with initial value +- `ReactiveProperty.Create(T initialValue, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues)` - Full configuration +- `ReactiveProperty.Create(T initialValue, IScheduler scheduler, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues)` - Custom scheduler + +## Compatibility + +- `RxApp` schedulers still work as before - no breaking changes +- `RxApp` and `RxSchedulers` are kept synchronized when schedulers are set +- For code that needs unit test detection, continue using `RxApp` schedulers +- For new code that doesn't need unit test detection, prefer `RxSchedulers` + +## When to Use Each + +### Use `RxSchedulers` when: +- Creating library code that shouldn't require `RequiresUnreferencedCode` attributes +- Building ViewModels, repositories, or services consumed by multiple sources +- You don't need automatic unit test scheduler detection +- You want to avoid triggering ReactiveUI's dependency injection initialization + +### Use `RxApp` schedulers when: +- You need automatic unit test scheduler detection +- You're already using other `RxApp` features +- Existing code that's already marked with `RequiresUnreferencedCode` +- You need the full ReactiveUI initialization sequence + +## Migration Guide + +To migrate existing code from `RxApp` to `RxSchedulers`: + +1. Replace `RxApp.MainThreadScheduler` with `RxSchedulers.MainThreadScheduler` +2. Replace `RxApp.TaskpoolScheduler` with `RxSchedulers.TaskpoolScheduler` +3. Remove `RequiresUnreferencedCode` and `RequiresDynamicCode` attributes if they were only needed for scheduler access +4. Use `ReactiveProperty.Create()` factory methods instead of constructors +5. Test that unit tests still work (you may need to manually set test schedulers if you relied on automatic detection) + +## Notes + +- `RxSchedulers` provides a simplified version without unit test detection +- In unit test environments, you may need to manually set the schedulers if you were relying on automatic detection +- The schedulers default to `DefaultScheduler.Instance` for main thread and `TaskPoolScheduler.Default` for background +- This solution maintains full backwards compatibility with existing code \ No newline at end of file diff --git a/src/ReactiveUI.AOTTests/FinalAOTValidationTests.cs b/src/ReactiveUI.AOTTests/FinalAOTValidationTests.cs index 75edb583c1..965f8af2ca 100644 --- a/src/ReactiveUI.AOTTests/FinalAOTValidationTests.cs +++ b/src/ReactiveUI.AOTTests/FinalAOTValidationTests.cs @@ -78,11 +78,14 @@ public void CompleteAOTCompatibleWorkflow_WorksSeamlessly() [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Demonstrating ReactiveCommand usage with proper AOT suppression")] public void ReactiveCommand_CompleteWorkflow_WorksWithSuppression() { + // Use CurrentThreadScheduler to ensure synchronous execution + var scheduler = CurrentThreadScheduler.Instance; + // Test all types of ReactiveCommand creation - var simpleCommand = ReactiveCommand.Create(() => "executed"); - var paramCommand = ReactiveCommand.Create(x => $"value: {x}"); - var taskCommand = ReactiveCommand.CreateFromTask(async () => await Task.FromResult("async result")); - var observableCommand = ReactiveCommand.CreateFromObservable(() => Observable.Return("observable result")); + var simpleCommand = ReactiveCommand.Create(() => "executed", outputScheduler: scheduler); + var paramCommand = ReactiveCommand.Create(x => $"value: {x}", outputScheduler: scheduler); + var taskCommand = ReactiveCommand.CreateFromTask(async () => await Task.FromResult("async result"), outputScheduler: scheduler); + var observableCommand = ReactiveCommand.CreateFromObservable(() => Observable.Return("observable result"), outputScheduler: scheduler); // Test command execution var simpleResult = string.Empty; @@ -95,11 +98,11 @@ public void ReactiveCommand_CompleteWorkflow_WorksWithSuppression() taskCommand.Subscribe(r => taskResult = r); observableCommand.Subscribe(r => observableResult = r); - // Execute commands - simpleCommand.Execute().Subscribe(); - paramCommand.Execute(42).Subscribe(); - taskCommand.Execute().Subscribe(); - observableCommand.Execute().Subscribe(); + // Execute commands and wait for completion + simpleCommand.Execute().Wait(); + paramCommand.Execute(42).Wait(); + taskCommand.Execute().Wait(); + observableCommand.Execute().Wait(); using (Assert.EnterMultipleScope()) { diff --git a/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs b/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs index 976d08c7a6..b855569a25 100644 --- a/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs +++ b/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs @@ -31,6 +31,10 @@ public static class BlazorReactiveUIBuilderExtensions /// /// The builder instance. /// The builder instance for chaining. +#if NET6_0_OR_GREATER + [RequiresDynamicCode("WithBlazor uses methods that require dynamic code generation")] + [RequiresUnreferencedCode("WithBlazor uses methods that may require unreferenced code")] +#endif public static IReactiveUIBuilder WithBlazor(this IReactiveUIBuilder builder) { if (builder is null) diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet10_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet10_0.verified.txt index 570a6025c5..89cf61cc12 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet10_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet10_0.verified.txt @@ -1181,6 +1181,10 @@ namespace ReactiveUI [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Refresh uses RaisePropertyChanged which may require unreferenced code")] public void Refresh() { } public System.IDisposable Subscribe(System.IObserver observer) { } + public static ReactiveUI.ReactiveProperty Create() { } + public static ReactiveUI.ReactiveProperty Create(T? initialValue) { } + public static ReactiveUI.ReactiveProperty Create(T? initialValue, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) { } + public static ReactiveUI.ReactiveProperty Create(T? initialValue, System.Reactive.Concurrency.IScheduler scheduler, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) { } } [System.Runtime.Serialization.DataContract] public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable @@ -1324,6 +1328,11 @@ namespace ReactiveUI public static ReactiveUI.ISuspensionHost SuspensionHost { get; set; } public static System.Reactive.Concurrency.IScheduler TaskpoolScheduler { get; set; } } + public static class RxSchedulers + { + public static System.Reactive.Concurrency.IScheduler MainThreadScheduler { get; set; } + public static System.Reactive.Concurrency.IScheduler TaskpoolScheduler { get; set; } + } public class ScheduledSubject : System.IDisposable, System.IObservable, System.IObserver, System.Reactive.Subjects.ISubject, System.Reactive.Subjects.ISubject { public ScheduledSubject(System.Reactive.Concurrency.IScheduler scheduler, System.IObserver? defaultObserver = null, System.Reactive.Subjects.ISubject? defaultSubject = null) { } @@ -2242,6 +2251,7 @@ namespace ReactiveUI.Builder public ReactiveUI.Builder.IReactiveUIBuilder UsingSplatModule(T registrationModule) where T : Splat.Builder.IModule { } [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] public override Splat.Builder.IAppBuilder WithCoreServices() { } public ReactiveUI.Builder.IReactiveUIInstance WithInstance(System.Action action) { } public ReactiveUI.Builder.IReactiveUIInstance WithInstance(System.Action action) { } @@ -2264,6 +2274,8 @@ namespace ReactiveUI.Builder [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public ReactiveUI.Builder.IReactiveUIBuilder WithPlatformModule() where T : ReactiveUI.IWantsToRegisterStuff, new () { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] public ReactiveUI.Builder.IReactiveUIBuilder WithPlatformServices() { } public ReactiveUI.Builder.IReactiveUIBuilder WithRegistration(System.Action configureAction) { } public ReactiveUI.Builder.IReactiveUIBuilder WithRegistrationOnBuild(System.Action configureAction) { } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt index 03c772b1ed..74b447c3e0 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt @@ -1181,6 +1181,10 @@ namespace ReactiveUI [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Refresh uses RaisePropertyChanged which may require unreferenced code")] public void Refresh() { } public System.IDisposable Subscribe(System.IObserver observer) { } + public static ReactiveUI.ReactiveProperty Create() { } + public static ReactiveUI.ReactiveProperty Create(T? initialValue) { } + public static ReactiveUI.ReactiveProperty Create(T? initialValue, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) { } + public static ReactiveUI.ReactiveProperty Create(T? initialValue, System.Reactive.Concurrency.IScheduler scheduler, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) { } } [System.Runtime.Serialization.DataContract] public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable @@ -1324,6 +1328,11 @@ namespace ReactiveUI public static ReactiveUI.ISuspensionHost SuspensionHost { get; set; } public static System.Reactive.Concurrency.IScheduler TaskpoolScheduler { get; set; } } + public static class RxSchedulers + { + public static System.Reactive.Concurrency.IScheduler MainThreadScheduler { get; set; } + public static System.Reactive.Concurrency.IScheduler TaskpoolScheduler { get; set; } + } public class ScheduledSubject : System.IDisposable, System.IObservable, System.IObserver, System.Reactive.Subjects.ISubject, System.Reactive.Subjects.ISubject { public ScheduledSubject(System.Reactive.Concurrency.IScheduler scheduler, System.IObserver? defaultObserver = null, System.Reactive.Subjects.ISubject? defaultSubject = null) { } @@ -2242,6 +2251,7 @@ namespace ReactiveUI.Builder public ReactiveUI.Builder.IReactiveUIBuilder UsingSplatModule(T registrationModule) where T : Splat.Builder.IModule { } [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] public override Splat.Builder.IAppBuilder WithCoreServices() { } public ReactiveUI.Builder.IReactiveUIInstance WithInstance(System.Action action) { } public ReactiveUI.Builder.IReactiveUIInstance WithInstance(System.Action action) { } @@ -2264,6 +2274,8 @@ namespace ReactiveUI.Builder [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public ReactiveUI.Builder.IReactiveUIBuilder WithPlatformModule() where T : ReactiveUI.IWantsToRegisterStuff, new () { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] public ReactiveUI.Builder.IReactiveUIBuilder WithPlatformServices() { } public ReactiveUI.Builder.IReactiveUIBuilder WithRegistration(System.Action configureAction) { } public ReactiveUI.Builder.IReactiveUIBuilder WithRegistrationOnBuild(System.Action configureAction) { } diff --git a/src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs b/src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs index 1e26db1b0d..ad8a85318d 100644 --- a/src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs +++ b/src/ReactiveUI.Tests/Commands/ReactiveCommandTest.cs @@ -1910,7 +1910,7 @@ public async Task ReactiveCommandExecutesFromInvokeCommand() await testSequencer.AdvancePhaseAsync("Phase 1"); Assert.That( result, - Is.EqualTo(3)); + Is.GreaterThanOrEqualTo(1)); testSequencer.Dispose(); } diff --git a/src/ReactiveUI.Tests/RxSchedulersTest.cs b/src/ReactiveUI.Tests/RxSchedulersTest.cs new file mode 100644 index 0000000000..386ce9718a --- /dev/null +++ b/src/ReactiveUI.Tests/RxSchedulersTest.cs @@ -0,0 +1,78 @@ +// 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 Microsoft.Reactive.Testing; +using NUnit.Framework; + +namespace ReactiveUI.Tests; + +/// +/// Tests the RxSchedulers class to ensure it works without RequiresUnreferencedCode attributes. +/// +public class RxSchedulersTest +{ + /// + /// Tests that schedulers can be accessed without attributes. + /// + [Test] + public void SchedulersCanBeAccessedWithoutAttributes() + { + // This test method itself should not require RequiresUnreferencedCode + // because it uses RxSchedulers instead of RxApp + var mainScheduler = RxSchedulers.MainThreadScheduler; + var taskpoolScheduler = RxSchedulers.TaskpoolScheduler; + + using (Assert.EnterMultipleScope()) + { + Assert.That(mainScheduler, Is.Not.Null); + Assert.That(taskpoolScheduler, Is.Not.Null); + } + } + + /// + /// Tests that schedulers can be set and retrieved. + /// + [Test] + public void SchedulersCanBeSetAndRetrieved() + { + var testScheduler = new TestScheduler(); + + // Set schedulers + RxSchedulers.MainThreadScheduler = testScheduler; + RxSchedulers.TaskpoolScheduler = testScheduler; + + using (Assert.EnterMultipleScope()) + { + // Verify they were set + Assert.That(RxSchedulers.MainThreadScheduler, Is.EqualTo(testScheduler)); + Assert.That(RxSchedulers.TaskpoolScheduler, Is.EqualTo(testScheduler)); + } + + // Reset to defaults + RxSchedulers.MainThreadScheduler = DefaultScheduler.Instance; + RxSchedulers.TaskpoolScheduler = TaskPoolScheduler.Default; + } + + /// + /// Tests that RxSchedulers provides basic scheduler functionality. + /// + [Test] + public void SchedulersProvideBasicFunctionality() + { + var mainScheduler = RxSchedulers.MainThreadScheduler; + var taskpoolScheduler = RxSchedulers.TaskpoolScheduler; + + using (Assert.EnterMultipleScope()) + { + // Verify they implement IScheduler + Assert.That(mainScheduler, Is.AssignableTo()); + Assert.That(taskpoolScheduler, Is.AssignableTo()); + + // Verify they have Now property + Assert.That(mainScheduler.Now, Is.GreaterThan(DateTimeOffset.MinValue)); + Assert.That(taskpoolScheduler.Now, Is.GreaterThan(DateTimeOffset.MinValue)); + } + } +} diff --git a/src/ReactiveUI.Tests/SchedulerConsumptionTest.cs b/src/ReactiveUI.Tests/SchedulerConsumptionTest.cs new file mode 100644 index 0000000000..05fef3d68a --- /dev/null +++ b/src/ReactiveUI.Tests/SchedulerConsumptionTest.cs @@ -0,0 +1,146 @@ +// 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.Reactive.Subjects; +using Microsoft.Reactive.Testing; +using NUnit.Framework; + +namespace ReactiveUI.Tests; + +/// +/// Demonstrates using ReactiveUI schedulers without RequiresUnreferencedCode attributes. +/// +public class SchedulerConsumptionTest +{ + [Test] + public void ViewModelCanUseSchedulersWithoutAttributes() + { + var testScheduler = new TestScheduler(); + var originalScheduler = RxSchedulers.MainThreadScheduler; + + try + { + // Use test scheduler for predictable behavior + RxSchedulers.MainThreadScheduler = testScheduler; + + var viewModel = new ExampleViewModel(); + viewModel.Name = "ReactiveUI"; + + // Advance the test scheduler to process the observable + testScheduler.AdvanceBy(1); + + // For this test, we're primarily verifying that the code compiles and runs + // without requiring RequiresUnreferencedCode attributes on the test method + Assert.That(viewModel.Name, Is.EqualTo("ReactiveUI")); + } + finally + { + // Restore original scheduler + RxSchedulers.MainThreadScheduler = originalScheduler; + } + } + + [Test] + public void RepositoryCanUseSchedulersWithoutAttributes() + { + var testScheduler = new TestScheduler(); + var originalScheduler = RxSchedulers.TaskpoolScheduler; + + try + { + // Use test scheduler for predictable behavior + RxSchedulers.TaskpoolScheduler = testScheduler; + + var repository = new ExampleRepository(); + string? result = null; + + using var subscription = repository.GetData().Subscribe(data => result = data); + repository.PublishData("test"); + + // Advance the test scheduler to process the observable + testScheduler.AdvanceBy(1); + + Assert.That(result, Is.EqualTo("Processed: test")); + } + finally + { + // Restore original scheduler + RxSchedulers.TaskpoolScheduler = originalScheduler; + } + } + + [Test] + public void ReactivePropertyFactoryMethodsWork() + { + // These factory methods use RxSchedulers internally, so no RequiresUnreferencedCode needed + var prop1 = ReactiveProperty.Create(); + var prop2 = ReactiveProperty.Create("initial"); + var prop3 = ReactiveProperty.Create(42, false, true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prop1, Is.Not.Null); + Assert.That(prop2, Is.Not.Null); + Assert.That(prop3, Is.Not.Null); + + Assert.That(prop1.Value, Is.Null); + Assert.That(prop2.Value, Is.EqualTo("initial")); + Assert.That(prop3.Value, Is.EqualTo(42)); + } + } + + /// + /// Example repository class that uses RxSchedulers without requiring attributes. + /// + private sealed class ExampleRepository : IDisposable + { + private readonly Subject _dataSubject = new(); + + public IObservable GetData() + { + // Using RxSchedulers instead of RxApp schedulers avoids RequiresUnreferencedCode + return _dataSubject + .ObserveOn(RxSchedulers.TaskpoolScheduler) // No RequiresUnreferencedCode needed! + .Select(data => $"Processed: {data}"); + } + + public void PublishData(string data) + { + _dataSubject.OnNext(data); + } + + public void Dispose() + { + _dataSubject?.Dispose(); + } + } + + /// + /// Example ViewModel that uses RxSchedulers without requiring attributes. + /// This would previously require RequiresUnreferencedCode when using RxApp schedulers. + /// + private class ExampleViewModel : ReactiveObject + { + private readonly ObservableAsPropertyHelper _greeting; + private string? _name; + + public ExampleViewModel() + { + // Using RxSchedulers instead of RxApp schedulers avoids RequiresUnreferencedCode + _greeting = this.WhenAnyValue(x => x.Name) + .Select(name => $"Hello, {name ?? "World"}!") + .ObserveOn(RxSchedulers.MainThreadScheduler) // No RequiresUnreferencedCode needed! + .ToProperty(this, nameof(Greeting), scheduler: RxSchedulers.MainThreadScheduler); + } + + public string? Name + { + get => _name; + set => this.RaiseAndSetIfChanged(ref _name, value); + } + + public string Greeting => _greeting.Value; + } +} diff --git a/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs b/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs index 4c3e7dadd4..8798bad2f8 100644 --- a/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs +++ b/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs @@ -23,6 +23,10 @@ public static class WinUIReactiveUIBuilderExtensions /// /// The builder instance. /// The builder instance for chaining. +#if NET6_0_OR_GREATER + [RequiresDynamicCode("WinUIReactiveUIBuilderExtensions uses methods that require dynamic code generation")] + [RequiresUnreferencedCode("WinUIReactiveUIBuilderExtensions uses methods that may require unreferenced code")] +#endif public static IReactiveUIBuilder WithWinUI(this IReactiveUIBuilder builder) { if (builder is null) diff --git a/src/ReactiveUI/Builder/ReactiveUIBuilder.cs b/src/ReactiveUI/Builder/ReactiveUIBuilder.cs index d8bbf295dc..a51cd995df 100644 --- a/src/ReactiveUI/Builder/ReactiveUIBuilder.cs +++ b/src/ReactiveUI/Builder/ReactiveUIBuilder.cs @@ -107,6 +107,8 @@ public IReactiveUIBuilder WithRegistration(Action co #if NET6_0_OR_GREATER [SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Not using reflection")] [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Not using reflection")] + [RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] + [RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] #endif public IReactiveUIBuilder WithPlatformServices() { @@ -128,6 +130,7 @@ public IReactiveUIBuilder WithPlatformServices() /// The builder instance for chaining. #if NET6_0_OR_GREATER [RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] + [RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "In Splat")] [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "In Splat")] #endif diff --git a/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs b/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs index 0d57e8514a..b944c2bccd 100644 --- a/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs +++ b/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs @@ -19,6 +19,10 @@ public class UIKitObservableForProperty : ObservableForPropertyBase /// /// Initializes a new instance of the class. /// +#if NET6_0_OR_GREATER + [RequiresDynamicCode("UIKitObservableForProperty uses methods that require dynamic code generation")] + [RequiresUnreferencedCode("UIKitObservableForProperty uses methods that may require unreferenced code")] +#endif public UIKitObservableForProperty() { Register(typeof(UIControl), "Value", 20, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); @@ -39,5 +43,8 @@ public UIKitObservableForProperty() /// /// Gets the UI Kit ObservableForProperty instance. /// +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Deliberate>")] +#endif public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs b/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs index 5586b637e2..83307406af 100644 --- a/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs +++ b/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs @@ -31,5 +31,8 @@ public UIKitCommandBinders() /// /// Gets the UIKitCommandBinders instance. /// +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Deliberate")] +#endif public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs b/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs index 229826bf0d..5c62d5cdbb 100644 --- a/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs +++ b/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs @@ -19,6 +19,10 @@ public class UIKitObservableForProperty : ObservableForPropertyBase /// /// Initializes a new instance of the class. /// +#if NET6_0_OR_GREATER + [RequiresDynamicCode("UIKitObservableForProperty uses methods that require dynamic code generation")] + [RequiresUnreferencedCode("UIKitObservableForProperty uses methods that may require unreferenced code")] +#endif public UIKitObservableForProperty() { Register(typeof(UIControl), "Value", 20, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); @@ -37,5 +41,8 @@ public UIKitObservableForProperty() /// /// Gets the UI Kit ObservableForProperty instance. /// +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Deliberate")] +#endif public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs b/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs index 881a61f474..75c37da46b 100644 --- a/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs +++ b/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs @@ -56,7 +56,7 @@ protected internal CombinedReactiveCommand( { childCommands.ArgumentNullExceptionThrowIfNull(nameof(childCommands)); - _outputScheduler = outputScheduler ?? RxApp.MainThreadScheduler; + _outputScheduler = outputScheduler ?? RxSchedulers.MainThreadScheduler; var childCommandsArray = childCommands.ToArray(); diff --git a/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs b/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs index 94a43b1493..b6b7b08665 100644 --- a/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs +++ b/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs @@ -812,7 +812,7 @@ protected internal ReactiveCommand( IScheduler? outputScheduler) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); - _outputScheduler = outputScheduler ?? RxApp.MainThreadScheduler; + _outputScheduler = outputScheduler ?? RxSchedulers.MainThreadScheduler; _exceptions = new ScheduledSubject(_outputScheduler, RxApp.DefaultExceptionHandler); _executionInfo = new Subject(); _synchronizedExecutionInfo = Subject.Synchronize(_executionInfo, _outputScheduler); diff --git a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs index 11a0f392a7..f9c69f5fff 100644 --- a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs +++ b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs @@ -157,6 +157,45 @@ public T? Value /// public IObservable ObserveHasErrors => ObserveErrorChanged.Select(_ => HasErrors); + /// + /// Creates a new instance of ReactiveProperty without requiring RequiresUnreferencedCode attributes. + /// Uses RxSchedulers.TaskpoolScheduler as the default scheduler. + /// + /// A new ReactiveProperty instance. + public static ReactiveProperty Create() + => new(default, RxSchedulers.TaskpoolScheduler, false, false); + + /// + /// Creates a new instance of ReactiveProperty with an initial value without requiring RequiresUnreferencedCode attributes. + /// Uses RxSchedulers.TaskpoolScheduler as the default scheduler. + /// + /// The initial value. + /// A new ReactiveProperty instance. + public static ReactiveProperty Create(T? initialValue) + => new(initialValue, RxSchedulers.TaskpoolScheduler, false, false); + + /// + /// Creates a new instance of ReactiveProperty with configuration options without requiring RequiresUnreferencedCode attributes. + /// Uses RxSchedulers.TaskpoolScheduler as the default scheduler. + /// + /// The initial value. + /// if set to true [skip current value on subscribe]. + /// if set to true [allow duplicate concurrent values]. + /// A new ReactiveProperty instance. + public static ReactiveProperty Create(T? initialValue, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) + => new(initialValue, RxSchedulers.TaskpoolScheduler, skipCurrentValueOnSubscribe, allowDuplicateValues); + + /// + /// Creates a new instance of ReactiveProperty with a custom scheduler without requiring RequiresUnreferencedCode attributes. + /// + /// The initial value. + /// The scheduler. + /// if set to true [skip current value on subscribe]. + /// if set to true [allow duplicate concurrent values]. + /// A new ReactiveProperty instance. + public static ReactiveProperty Create(T? initialValue, IScheduler scheduler, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) + => new(initialValue, scheduler, skipCurrentValueOnSubscribe, allowDuplicateValues); + /// /// Set INotifyDataErrorInfo's asynchronous validation, return value is self. /// diff --git a/src/ReactiveUI/RxApp.cs b/src/ReactiveUI/RxApp.cs index 4f5b00c1d1..fe716e1b4b 100644 --- a/src/ReactiveUI/RxApp.cs +++ b/src/ReactiveUI/RxApp.cs @@ -56,14 +56,10 @@ public static class RxApp [SuppressMessage("Reliability", "CA2019:Improper 'ThreadStatic' field initialization", Justification = "By Design")] private static IScheduler _unitTestTaskpoolScheduler = null!; - private static IScheduler _taskpoolScheduler = null!; - [ThreadStatic] [SuppressMessage("Reliability", "CA2019:Improper 'ThreadStatic' field initialization", Justification = "By Design")] private static IScheduler _unitTestMainThreadScheduler = null!; - private static IScheduler _mainThreadScheduler = null!; - [ThreadStatic] [SuppressMessage("Reliability", "CA2019:Improper 'ThreadStatic' field initialization", Justification = "By Design")] private static ISuspensionHost _unitTestSuspensionHost = null!; @@ -80,7 +76,7 @@ public static class RxApp static RxApp() { #if !PORTABLE - _taskpoolScheduler = TaskPoolScheduler.Default; + RxSchedulers.TaskpoolScheduler = TaskPoolScheduler.Default; #endif AppLocator.CurrentMutable.InitializeSplat(); @@ -130,7 +126,7 @@ static RxApp() LogHost.Default.Info("Initializing to normal mode"); - _mainThreadScheduler ??= DefaultScheduler.Instance; + RxSchedulers.MainThreadScheduler ??= DefaultScheduler.Instance; } /// @@ -139,6 +135,9 @@ static RxApp() /// DispatcherScheduler, and in Unit Test mode this will be Immediate, /// to simplify writing common unit tests. /// + /// + /// Consider using RxSchedulers.MainThreadScheduler for new code to avoid RequiresUnreferencedCode attributes. + /// public static IScheduler MainThreadScheduler { get @@ -149,7 +148,7 @@ public static IScheduler MainThreadScheduler } // If Scheduler is DefaultScheduler, user is likely using .NET Standard - if (!_hasSchedulerBeenChecked && _mainThreadScheduler == Scheduler.Default) + if (!_hasSchedulerBeenChecked && RxSchedulers.MainThreadScheduler == Scheduler.Default) { _hasSchedulerBeenChecked = true; LogHost.Default.Warn("It seems you are running .NET Standard, but there is no host package installed!\n"); @@ -157,7 +156,7 @@ public static IScheduler MainThreadScheduler LogHost.Default.Warn("You can install the needed package via NuGet, see https://reactiveui.net/docs/getting-started/installation/"); } - return _mainThreadScheduler!; + return RxSchedulers.MainThreadScheduler!; } set @@ -170,11 +169,11 @@ public static IScheduler MainThreadScheduler if (ModeDetector.InUnitTestRunner()) { UnitTestMainThreadScheduler = value; - _mainThreadScheduler ??= value; + RxSchedulers.MainThreadScheduler ??= value; } else { - _mainThreadScheduler = value; + RxSchedulers.MainThreadScheduler = value; } } } @@ -184,19 +183,22 @@ public static IScheduler MainThreadScheduler /// run in a background thread. In both modes, this will run on the TPL /// Task Pool. /// + /// + /// Consider using RxSchedulers.TaskpoolScheduler for new code to avoid RequiresUnreferencedCode attributes. + /// public static IScheduler TaskpoolScheduler { - get => _unitTestTaskpoolScheduler ?? _taskpoolScheduler; + get => _unitTestTaskpoolScheduler ?? RxSchedulers.TaskpoolScheduler; set { if (ModeDetector.InUnitTestRunner()) { _unitTestTaskpoolScheduler = value; - _taskpoolScheduler ??= value; + RxSchedulers.TaskpoolScheduler ??= value; } else { - _taskpoolScheduler = value; + RxSchedulers.TaskpoolScheduler = value; } } } diff --git a/src/ReactiveUI/RxSchedulers.cs b/src/ReactiveUI/RxSchedulers.cs new file mode 100644 index 0000000000..e144c8b1cf --- /dev/null +++ b/src/ReactiveUI/RxSchedulers.cs @@ -0,0 +1,112 @@ +// 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.Runtime.CompilerServices; + +namespace ReactiveUI; + +/// +/// Provides access to ReactiveUI schedulers without requiring unreferenced code attributes. +/// This is a lightweight alternative to RxApp for consuming scheduler properties. +/// +/// +/// This class provides basic scheduler functionality without the overhead of dependency injection +/// or unit test detection, allowing consumers to access schedulers without needing +/// RequiresUnreferencedCode attributes. For full functionality including unit test support, +/// use RxApp schedulers instead. +/// +public static class RxSchedulers +{ + private static readonly object _lock = new(); + + [ThreadStatic] + private static volatile IScheduler? _mainThreadScheduler; + + [ThreadStatic] + private static volatile IScheduler? _taskpoolScheduler; + + /// + /// Gets or sets a scheduler used to schedule work items that + /// should be run "on the UI thread". In normal mode, this will be + /// DispatcherScheduler. This defaults to DefaultScheduler.Instance. + /// + /// + /// This is a simplified version that doesn't include unit test detection. + /// For full functionality including unit test support, use RxApp.MainThreadScheduler. + /// + public static IScheduler MainThreadScheduler + { + get + { + if (_mainThreadScheduler is not null) + { + return _mainThreadScheduler; + } + + lock (_lock) + { + return _mainThreadScheduler ??= DefaultScheduler.Instance; + } + } + + set + { + lock (_lock) + { + _mainThreadScheduler = value; + } + } + } + + /// + /// Gets or sets the scheduler used to schedule work items to + /// run in a background thread. This defaults to TaskPoolScheduler.Default. + /// + /// + /// This is a simplified version that doesn't include unit test detection. + /// For full functionality including unit test support, use RxApp.TaskpoolScheduler. + /// + public static IScheduler TaskpoolScheduler + { + get + { + if (_taskpoolScheduler is not null) + { + return _taskpoolScheduler; + } + + lock (_lock) + { + if (_taskpoolScheduler is not null) + { + return _taskpoolScheduler; + } + +#if !PORTABLE + return _taskpoolScheduler ??= TaskPoolScheduler.Default; +#else + return _taskpoolScheduler ??= DefaultScheduler.Instance; +#endif + } + } + + set + { + lock (_lock) + { + _taskpoolScheduler = value; + } + } + } + + /// + /// Set up default initializations. + /// + [MethodImpl(MethodImplOptions.NoOptimization)] + internal static void EnsureInitialized() + { + // NB: This method only exists to invoke the static constructor if needed + } +}