From d170bab6044c9b7608437d4508fa2f630deab3a2 Mon Sep 17 00:00:00 2001 From: Glenn Watson <5834289+glennawatson@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:45:38 +1000 Subject: [PATCH 1/4] feat(observation): add WhenAnyDynamic for chains named at run time - Observe a property chain given as an Expression the caller built, across arities 1 to 12, with and without the distinct gate. - The chain is walked by reflection, so every overload carries [RequiresUnreferencedCode]. This is the one part of the observation surface that is not trimming- or AOT-safe. - Every arity observes each of its chains through one shared helper and differs only in how many it combines. --- README.md | 53 + ...ctiveUIBindingExtensions.WhenAnyDynamic.cs | 1030 +++++++++++++++++ .../RxUiDynamicChainBaseline.cs | 142 +++ .../WhenAnyDynamicBenchmark.cs | 140 +++ .../Mixins/WhenAnyDynamicTests.cs | 983 ++++++++++++++++ .../TestModels/DynamicChainChild.cs | 26 + .../TestModels/DynamicChainFixture.cs | 158 +++ 7 files changed, 2532 insertions(+) create mode 100644 src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs create mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs create mode 100644 src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs diff --git a/README.md b/README.md index bbaa33a..27483f5 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Platform-specific packages provide DependencyProperty observation and other plat | `WhenChanging` | Observe property changes (before value changes, requires `INotifyPropertyChanging`) | | `WhenAnyValue` | ReactiveUI compatibility shim -- same semantics as `WhenChanged` | | `WhenAny` | Multi-property observation with selector | +| `WhenAnyDynamic` | Observe a chain given as an `Expression` built at run time (reflection, not AOT-safe) | | `WhenAnyObservable` | Observe and switch between observable properties | | `BindOneWay` | One-way binding from source to target | | `BindTwoWay` | Two-way binding between source and target | @@ -178,6 +179,28 @@ IObservable fullName = vm.WhenChanged( IObservable nameChanging = vm.WhenChanging(x => x.Name); ``` +### Observing a Chain Named at Run Time + +`WhenAnyDynamic` takes the chain as a `System.Linq.Expressions.Expression` the caller built, rather than as a +lambda the compiler could read. Arities 1 to 12 are available, each with and without the distinct gate. + +```csharp +// The chain is a value, so it can be assembled from a property name, a configuration entry, or a caller. +Expression chain = ((Expression>)(x => x.Address.City)).Body; + +IObservable cityObs = vm.WhenAnyDynamic(chain, static c => (string?)c.Value); + +IObservable fullName = vm.WhenAnyDynamic( + firstNameChain, + lastNameChain, + static (first, last) => $"{first.Value} {last.Value}"); +``` + +There is nothing for a generator to resolve in an expression built at run time, so the chain is walked by +reflection. Every overload carries `[RequiresUnreferencedCode]`, which makes this the one part of the observation +surface that is not trimming- or AOT-safe and reports each call site in a `PublishAot` build. Where the chain is +known at compile time, `WhenChanged` and `WhenAny` observe the same thing with no reflection. + ### One-Way Binding ```csharp @@ -413,6 +436,36 @@ subscription rather than re-projecting the two observed sides: | Two-Way Binding (.NET 10.0) | 8.1x faster | 8.5x less | | First Binding (.NET 10.0) | 4.1x faster | 9.6x less | +#### Chains Named at Run Time (WhenAnyDynamic) + +| Method | Runtime | Mean | Allocated | +|-------------------|-----------|-------:|----------:| +| Single Chain | .NET 10.0 | 278 us | 102.7 KB | +| Two Chains | .NET 10.0 | 595 us | 190.2 KB | +| Deep Chain | .NET 10.0 | 354 us | 103.3 KB | +| First Observation | .NET 10.0 | 7.3 us | 1.1 KB | +| Single Chain | .NET 8.0 | 347 us | 102.7 KB | +| Two Chains | .NET 8.0 | 691 us | 190.1 KB | +| Deep Chain | .NET 8.0 | 419 us | 103.3 KB | +| First Observation | .NET 8.0 | 7.9 us | 1.1 KB | + +This is the one part of the surface where the two engines are level. Both walk the chain by reflection and +allocate the same objects doing it, so the numbers land on top of each other; the gain is in resolving the chain +at compile time instead: + +| Single chain, .NET 10.0 | Mean | Allocated | +|-----------------------------|-------:|----------:| +| Generated (`WhenChanged`) | 175 us | 63.4 KB | +| `WhenAnyDynamic` | 278 us | 102.7 KB | +| ReactiveUI `WhenAnyDynamic` | 299 us | 102.6 KB | + +Most of the gap to the generated path is the `IObservedChange` handed to the selector - 40 bytes per +notification, which the signature has to produce, against a generated observation that emits the value itself. + +Two chains fire twice the notifications of one, so their 190 KB is 95 bytes per notification against a single +chain's 103: combining a higher arity costs one subscription, not a per-notification charge. Every arity observes +each of its chains through the same walk and differs only in how many it combines. + ## Diagnostics The separate analyzer package reports the following diagnostics: diff --git a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs new file mode 100644 index 0000000..44bc316 --- /dev/null +++ b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs @@ -0,0 +1,1030 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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; + +#if REACTIVE_SHIM +namespace ReactiveUI.Binding.Reactive; +#else +namespace ReactiveUI.Binding; +#endif + +/// Observes property chains that are only known as expressions at run time. +/// +/// Every other observation in this library is resolved at compile time, which is what keeps it free of +/// reflection. These overloads take an the caller built rather than a lambda the +/// compiler could read, so there is nothing for a generator to resolve and the chain has to be walked by +/// reflection. They are the one part of the observation surface that is not trim- or AOT-safe, and they +/// say so. +/// +/// The walking itself is the engine the runtime observation fallback already uses, so resolving a link, +/// re-subscribing a deeper one when an intermediate moves, and filtering duplicates are not written twice. +/// Each arity differs only in how many chains it combines. +/// +/// +public static partial class ReactiveUIBindingExtensions +{ + /// Why every overload here is unsafe to trim. + private const string DynamicChainRequiresUnreferencedCode = + "Evaluates expression-based member chains via reflection; members may be trimmed."; + + /// Observes 1 dynamically-typed property chain and projects it with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Func, TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, selector, true); + + /// Observes 1 dynamically-typed property chain and projects it with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Func, TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return ObserveDynamicChain(sender, property1, isDistinct).Select(selector); + } + + /// Observes 2 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Func, IObservedChange, TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, selector, true); + + /// Observes 2 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Func, IObservedChange, TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + selector); + } + + /// Observes 3 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Func, IObservedChange, IObservedChange, TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, selector, true); + + /// Observes 3 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Func, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + selector); + } + + /// Observes 4 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, selector, true); + + /// Observes 4 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + selector); + } + + /// Observes 5 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, selector, true); + + /// Observes 5 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + selector); + } + + /// Observes 6 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, selector, true); + + /// Observes 6 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + selector); + } + + /// Observes 7 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, selector, true); + + /// Observes 7 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + ObserveDynamicChain(sender, property7, isDistinct), + selector); + } + + /// Observes 8 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, selector, true); + + /// Observes 8 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + ObserveDynamicChain(sender, property7, isDistinct), + ObserveDynamicChain(sender, property8, isDistinct), + selector); + } + + /// Observes 9 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, selector, true); + + /// Observes 9 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + ObserveDynamicChain(sender, property7, isDistinct), + ObserveDynamicChain(sender, property8, isDistinct), + ObserveDynamicChain(sender, property9, isDistinct), + selector); + } + + /// Observes 10 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, selector, true); + + /// Observes 10 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + ObserveDynamicChain(sender, property7, isDistinct), + ObserveDynamicChain(sender, property8, isDistinct), + ObserveDynamicChain(sender, property9, isDistinct), + ObserveDynamicChain(sender, property10, isDistinct), + selector); + } + + /// Observes 11 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, selector, true); + + /// Observes 11 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + ObserveDynamicChain(sender, property7, isDistinct), + ObserveDynamicChain(sender, property8, isDistinct), + ObserveDynamicChain(sender, property9, isDistinct), + ObserveDynamicChain(sender, property10, isDistinct), + ObserveDynamicChain(sender, property11, isDistinct), + selector); + } + + /// Observes 12 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// An expression naming property 12. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Expression? property12, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12, selector, true); + + /// Observes 12 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// An expression naming property 12. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Expression? property12, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + return CombineLatestObservable.Create( + ObserveDynamicChain(sender, property1, isDistinct), + ObserveDynamicChain(sender, property2, isDistinct), + ObserveDynamicChain(sender, property3, isDistinct), + ObserveDynamicChain(sender, property4, isDistinct), + ObserveDynamicChain(sender, property5, isDistinct), + ObserveDynamicChain(sender, property6, isDistinct), + ObserveDynamicChain(sender, property7, isDistinct), + ObserveDynamicChain(sender, property8, isDistinct), + ObserveDynamicChain(sender, property9, isDistinct), + ObserveDynamicChain(sender, property10, isDistinct), + ObserveDynamicChain(sender, property11, isDistinct), + ObserveDynamicChain(sender, property12, isDistinct), + selector); + } + + /// Subscribes to one property chain named by a run-time expression. + /// The type of the object the chain is rooted on. + /// The object the chain is rooted on. + /// An expression naming the chain. + /// Whether the chain reports only when its value changes. + /// An observable of the chain's observed changes. + /// Every arity funnels through here, so the reflection is written once. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IObservable> ObserveDynamicChain( + TSender sender, + Expression? property, + bool isDistinct) + where TSender : class => + sender.SubscribeToExpressionChain(property, false, false, isDistinct); +} diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs new file mode 100644 index 0000000..ba5739e --- /dev/null +++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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.Linq.Expressions; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using ReactiveUI; +using ReactiveUI.Builder; +using BenchmarkVm = ReactiveUI.Binding.Benchmarks.BenchmarkViewModel; + +namespace RxUiDynamicChain; + +/// +/// The same dynamic-chain scenarios as WhenAnyDynamicBenchmark, run against ReactiveUI's own engine so +/// the two are read side by side. +/// +/// +/// Declared outside the ReactiveUI.Binding namespace on purpose. Extension lookup walks the enclosing +/// namespaces from the inside out and stops at the first level offering a candidate, so a class nested under +/// ReactiveUI.Binding reaches this library's overloads and never consults ReactiveUI's. The view model +/// arrives through a type alias rather than an import for the same reason: importing its namespace would put +/// both libraries' overloads at the outermost level and make every call ambiguous. +/// +[SimpleJob(RuntimeMoniker.Net80)] +[SimpleJob(RuntimeMoniker.Net10_0)] +[MemoryDiagnoser] +[EventPipeProfiler(EventPipeProfile.GcVerbose)] +[MarkdownExporterAttribute.GitHub] +public class RxUiDynamicChainBaseline +{ + /// Represents the number of property change events to be triggered during the benchmark tests. + private const int PropertyChangeCount = 1_000; + + /// Names the chain reaching one property. + private static readonly Expression NameChain = Chain(x => x.Name); + + /// Names the chain reaching a second property, so an arity above one has something to combine. + private static readonly Expression AgeChain = Chain(x => x.Age); + + /// Names the chain reaching through an intermediate. + private static readonly Expression ChildValueChain = Chain(x => x.Child.Value); + + /// Reads one observed change. + /// + /// Typed rather than inferred so the binding is pinned: the parameter names ReactiveUI's + /// , so a call that resolved to this library's overload of + /// the same name would not compile rather than quietly benchmarking the wrong engine. + /// + private static readonly Func, object?> ReadOne = + static c1 => c1.Value; + + /// Reads whichever of two observed changes carries a value. + private static readonly Func, IObservedChange, object?> ReadEither = + static (c1, c2) => c1.Value ?? c2.Value; + + /// The view model instance used for observation benchmarks. + private BenchmarkVm _vm = null!; + + /// Registers the observation plugins ReactiveUI resolves each link through. + [GlobalSetup] + public void Register() + { + var builder = RxAppBuilder.CreateReactiveUIBuilder(); + _ = builder.WithCoreServices(); + _ = builder.BuildApp(); + } + + /// Sets up a fresh view model before each benchmark iteration. + [IterationSetup] + public void Setup() => + _vm = new() { Name = "Initial", Age = 0, Child = new() { Value = "ChildInitial" } }; + + /// One chain: subscribe, fire N changes, dispose. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "Single Chain")] + public void SingleChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, ReadOne) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + } + } + + /// Two chains combined: subscribe, fire N changes on each, dispose. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "Two Chains")] + public void TwoChains() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, AgeChain, ReadEither) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + _vm.Age = i; + } + } + + /// A chain through an intermediate: subscribe, fire N changes on the leaf, dispose. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "Deep Chain")] + public void DeepChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(ChildValueChain, ReadOne) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Child.Value = $"Value_{i}"; + } + } + + /// Cold start: subscribe, read the initial value, dispose. No property changes. + /// The observed value. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "First Observation")] + public object? FirstObservation() + { + object? result = null; + using var sub = _vm.WhenAnyDynamic(NameChain, ReadOne) + .Subscribe(v => result = v); + return result; + } + + /// Names a property chain the way a caller building one at run time hands it over. + /// The type the chain ends at. + /// The chain to name. + /// The expression body, which is what these overloads take. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Expression Chain(Expression> property) => + property.Body; +} diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs new file mode 100644 index 0000000..2f547b4 --- /dev/null +++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs @@ -0,0 +1,140 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated 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.Linq.Expressions; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using ReactiveUI.Binding.Builder; + +namespace ReactiveUI.Binding.Benchmarks; + +/// +/// Reflection-walked observation benchmarks, and what the generated observation of the same chain costs. +/// The pair is the choice a caller actually makes: a chain named by an expression built at run time cannot +/// be resolved at compile time, and this says what that buys and what it costs. +/// +/// +/// No NativeAOT job: these overloads walk members by reflection and say so with +/// , so an ahead-of-time published run is not a +/// configuration they support. +/// +[SimpleJob(RuntimeMoniker.Net80)] +[SimpleJob(RuntimeMoniker.Net10_0)] +[MemoryDiagnoser] +[EventPipeProfiler(EventPipeProfile.GcVerbose)] +[MarkdownExporterAttribute.GitHub] +public class WhenAnyDynamicBenchmark +{ + /// Represents the number of property change events to be triggered during the benchmark tests. + private const int PropertyChangeCount = 1_000; + + /// Names the chain reaching one property. + private static readonly Expression NameChain = Chain(x => x.Name); + + /// Names the chain reaching a second property, so an arity above one has something to combine. + private static readonly Expression AgeChain = Chain(x => x.Age); + + /// Names the chain reaching through an intermediate. + private static readonly Expression ChildValueChain = Chain(x => x.Child.Value); + + /// The view model instance used for observation benchmarks. + private BenchmarkViewModel _vm = null!; + + /// Registers the observation plugins the reflection walk resolves each link through. + [GlobalSetup] + public void Register() + { + var builder = RxBindingBuilder.CreateReactiveUIBindingBuilder(); + _ = builder.WithCoreServices(); + _ = builder.BuildApp(); + } + + /// Sets up a fresh view model before each benchmark iteration. + [IterationSetup] + public void Setup() => + _vm = new() { Name = "Initial", Age = 0, Child = new() { Value = "ChildInitial" } }; + + /// One chain: subscribe, fire N changes, dispose. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "Single Chain")] + public void SingleChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, static c1 => c1.Value) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + } + } + + /// Two chains combined: subscribe, fire N changes on each, dispose. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "Two Chains")] + public void TwoChains() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, AgeChain, static (c1, c2) => c1.Value ?? c2.Value) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + _vm.Age = i; + } + } + + /// A chain through an intermediate: subscribe, fire N changes on the leaf, dispose. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "Deep Chain")] + public void DeepChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(ChildValueChain, static c1 => c1.Value) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Child.Value = $"Value_{i}"; + } + } + + /// Cold start: subscribe, read the initial value, dispose. No property changes. + /// The observed value. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [Benchmark(Description = "First Observation")] + public object? FirstObservation() + { + object? result = null; + using var sub = _vm.WhenAnyDynamic(NameChain, static c1 => c1.Value) + .Subscribe(v => result = v); + return result; + } + + /// The same single chain resolved at compile time, which is what the reflection walk is weighed against. + [Benchmark(Description = "Single Chain (generated)", Baseline = true)] + public void SingleChainGenerated() + { + var last = string.Empty; + using var sub = _vm.WhenChanged(x => x.Name) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + } + } + + /// Names a property chain the way a caller building one at run time hands it over. + /// The type the chain ends at. + /// The chain to name. + /// The expression body, which is what these overloads take. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Expression Chain(Expression> property) => + property.Body; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs new file mode 100644 index 0000000..fe1c15f --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs @@ -0,0 +1,983 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// +/// Tests for the WhenAnyDynamic overloads, which observe property chains named by an expression the +/// caller built rather than by a lambda the compiler could read. +/// +public class WhenAnyDynamicTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The value the last observed property is moved to. + private const string MovedValue = "b"; + + /// How many chains the one-chain overloads observe. + private const int OneChain = 1; + + /// How many chains the two-chain overloads observe. + private const int TwoChains = 2; + + /// How many chains the three-chain overloads observe. + private const int ThreeChains = 3; + + /// How many chains the four-chain overloads observe. + private const int FourChains = 4; + + /// How many chains the five-chain overloads observe. + private const int FiveChains = 5; + + /// How many chains the six-chain overloads observe. + private const int SixChains = 6; + + /// How many chains the seven-chain overloads observe. + private const int SevenChains = 7; + + /// How many chains the eight-chain overloads observe. + private const int EightChains = 8; + + /// How many chains the nine-chain overloads observe. + private const int NineChains = 9; + + /// How many chains the ten-chain overloads observe. + private const int TenChains = 10; + + /// How many chains the eleven-chain overloads observe. + private const int ElevenChains = 11; + + /// How many chains the twelve-chain overloads observe. + private const int TwelveChains = 12; + + /// What a chain reports with the distinct gate on: the seed, then the move. + private static readonly string[] SeedThenMove = [InitialValue, MovedValue]; + + /// What a chain reports with the gate off: the seed, the move, and the notification repeating it. + private static readonly string[] SeedThenMoveTwice = [InitialValue, MovedValue, MovedValue]; + + /// Observing 1 chain, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity1_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + static c1 => string.Concat(c1.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(OneChain)); + + fixture.P1 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(OneChain)); + } + + /// Observing 1 chain with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity1WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + static c1 => string.Concat(c1.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(OneChain)); + + fixture.P1 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(OneChain)); + } + + /// Observing 2 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity2_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + static (c1, c2) => string.Concat(c1.Value, c2.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwoChains)); + + fixture.P2 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwoChains)); + } + + /// Observing 2 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity2WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + static (c1, c2) => string.Concat(c1.Value, c2.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwoChains)); + + fixture.P2 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwoChains)); + } + + /// Observing 3 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity3_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + static (c1, c2, c3) => string.Concat(c1.Value, c2.Value, c3.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ThreeChains)); + + fixture.P3 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ThreeChains)); + } + + /// Observing 3 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity3WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + static (c1, c2, c3) => string.Concat(c1.Value, c2.Value, c3.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ThreeChains)); + + fixture.P3 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ThreeChains)); + } + + /// Observing 4 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity4_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + static (c1, c2, c3, c4) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FourChains)); + + fixture.P4 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FourChains)); + } + + /// Observing 4 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity4WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + static (c1, c2, c3, c4) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FourChains)); + + fixture.P4 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FourChains)); + } + + /// Observing 5 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity5_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + static (c1, c2, c3, c4, c5) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FiveChains)); + + fixture.P5 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FiveChains)); + } + + /// Observing 5 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity5WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + static (c1, c2, c3, c4, c5) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FiveChains)); + + fixture.P5 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FiveChains)); + } + + /// Observing 6 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity6_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + static (c1, c2, c3, c4, c5, c6) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SixChains)); + + fixture.P6 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SixChains)); + } + + /// Observing 6 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity6WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + static (c1, c2, c3, c4, c5, c6) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SixChains)); + + fixture.P6 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SixChains)); + } + + /// Observing 7 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity7_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + static (c1, c2, c3, c4, c5, c6, c7) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SevenChains)); + + fixture.P7 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SevenChains)); + } + + /// Observing 7 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity7WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + static (c1, c2, c3, c4, c5, c6, c7) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SevenChains)); + + fixture.P7 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SevenChains)); + } + + /// Observing 8 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity8_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + static (c1, c2, c3, c4, c5, c6, c7, c8) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(EightChains)); + + fixture.P8 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(EightChains)); + } + + /// Observing 8 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity8WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + static (c1, c2, c3, c4, c5, c6, c7, c8) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(EightChains)); + + fixture.P8 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(EightChains)); + } + + /// Observing 9 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity9_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(NineChains)); + + fixture.P9 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(NineChains)); + } + + /// Observing 9 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity9WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(NineChains)); + + fixture.P9 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(NineChains)); + } + + /// Observing 10 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity10_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TenChains)); + + fixture.P10 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TenChains)); + } + + /// Observing 10 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity10WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TenChains)); + + fixture.P10 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TenChains)); + } + + /// Observing 11 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity11_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ElevenChains)); + + fixture.P11 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ElevenChains)); + } + + /// Observing 11 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity11WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ElevenChains)); + + fixture.P11 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ElevenChains)); + } + + /// Observing 12 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity12_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + Body(x => x.P12), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value, + c12.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwelveChains)); + + fixture.P12 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwelveChains)); + } + + /// Observing 12 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity12WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + Body(x => x.P12), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value, + c12.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwelveChains)); + + fixture.P12 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwelveChains)); + } + + /// The distinct gate suppresses a notification that leaves the value where it was. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithTheDistinctGateOn_SuppressesADuplicate() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture + .WhenAnyDynamic(Body(x => x.P1), static c1 => string.Concat(c1.Value), true) + .Subscribe(seen.Add); + + fixture.P1 = MovedValue; + fixture.P1 = MovedValue; + + await Assert.That(seen).IsEquivalentTo(SeedThenMove); + } + + /// With the gate off, a notification is reported even where the value did not move. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithTheDistinctGateOff_ReportsADuplicate() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture + .WhenAnyDynamic(Body(x => x.P1), static c1 => string.Concat(c1.Value), false) + .Subscribe(seen.Add); + + fixture.P1 = MovedValue; + fixture.P1 = MovedValue; + + await Assert.That(seen).IsEquivalentTo(SeedThenMoveTwice); + } + + /// A chain through an intermediate follows the intermediate when it is replaced. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_ThroughAnIntermediate_FollowsTheReplacement() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture + .WhenAnyDynamic(Body(x => x.Child!.Name), static c1 => string.Concat(c1.Value)) + .Subscribe(seen.Add); + + fixture.Child = new DynamicChainChild { Name = MovedValue }; + + await Assert.That(seen[^1]).IsEqualTo(MovedValue); + } + + /// An object to observe is required. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithNoSender_ThrowsArgumentNullException() + { + DynamicChainFixture? fixture = null; + + await Assert.That(() => fixture!.WhenAnyDynamic(Body(x => x.P1), static c1 => string.Concat(c1.Value))) + .ThrowsExactly(); + } + + /// A selector is required. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithNoSelector_ThrowsArgumentNullException() + { + var fixture = new DynamicChainFixture(); + + await Assert.That(() => fixture.WhenAnyDynamic(Body(x => x.P1), null!)) + .ThrowsExactly(); + } + + /// The combined value while every observed chain still holds its initial value. + /// How many chains were observed. + /// What the selector concatenates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string Initial(int count) => string.Concat(Enumerable.Repeat(InitialValue, count)); + + /// The combined value once the last of the observed chains has moved. + /// How many chains were observed. + /// What the selector concatenates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string LastMoved(int count) => Initial(count - 1) + MovedValue; + + /// Names a property chain the way a caller builds one at run time. + /// The type the chain ends at. + /// The chain to name. + /// The expression body, which is what these overloads take. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static System.Linq.Expressions.Expression Body( + Expression> property) => + property.Body; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs new file mode 100644 index 0000000..3f88a2e --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object reached through an intermediate link of an observed chain. +/// Public because the chain is walked by reflection, which reads public members. +public class DynamicChainChild : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets the observed value. + public string Name + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Name))); + } + } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs new file mode 100644 index 0000000..7553d7f --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs @@ -0,0 +1,158 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object with enough notifying properties to observe every dynamic-chain arity. +/// Public because the chain is walked by reflection, which reads public members. +public class DynamicChainFixture : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets observed property 1. + public string P1 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P1))); + } + } = "a"; + + /// Gets or sets observed property 2. + public string P2 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P2))); + } + } = "a"; + + /// Gets or sets observed property 3. + public string P3 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P3))); + } + } = "a"; + + /// Gets or sets observed property 4. + public string P4 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P4))); + } + } = "a"; + + /// Gets or sets observed property 5. + public string P5 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P5))); + } + } = "a"; + + /// Gets or sets observed property 6. + public string P6 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P6))); + } + } = "a"; + + /// Gets or sets observed property 7. + public string P7 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P7))); + } + } = "a"; + + /// Gets or sets observed property 8. + public string P8 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P8))); + } + } = "a"; + + /// Gets or sets observed property 9. + public string P9 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P9))); + } + } = "a"; + + /// Gets or sets observed property 10. + public string P10 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P10))); + } + } = "a"; + + /// Gets or sets observed property 11. + public string P11 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P11))); + } + } = "a"; + + /// Gets or sets observed property 12. + public string P12 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P12))); + } + } = "a"; + + /// Gets or sets the intermediate link an observed chain passes through. + public DynamicChainChild? Child + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Child))); + } + } +} From 414228c4d4a22af8bb25af171a59626591d2287b Mon Sep 17 00:00:00 2001 From: Glenn Watson <5834289+glennawatson@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:45:46 +1000 Subject: [PATCH 2/4] test(observation): cover every runtime observation and binding overload - Drop the class-level ExcludeFromCodeCoverage from ReactiveUIBindingExtensions. A partial's attribute applies to the merged type, so one attribute on the file holding the class declaration hid all fifteen partials - 154 real methods, of which only 17 are the refusing dispatch stubs it was written for. - Cover every arity of WhenChanged, WhenChanging, WhenAnyValue, WhenAny and WhenAnyObservable, and each binding stub's refusal. - Reach the overloads on the declaring class rather than as extension methods: written as an extension call the generated dispatch wins overload resolution and the runtime overload never runs. --- .../Mixins/ReactiveUIBindingExtensions.cs | 1 - .../Mixins/BindingDispatchStubTests.cs | 221 +++++ .../Mixins/WhenAnyObservableWideArityTests.cs | 740 +++++++++++++++ .../Mixins/WhenAnyValueWideArityTests.cs | 854 +++++++++++++++++ .../Mixins/WhenAnyWideArityTests.cs | 316 ++++++ .../Mixins/WhenChangedWideArityTests.cs | 835 ++++++++++++++++ .../Mixins/WhenChangingWideArityTests.cs | 897 ++++++++++++++++++ .../TestModels/DispatchStubControl.cs | 12 + .../TestModels/DispatchStubView.cs | 31 + .../TestModels/DispatchStubViewModel.cs | 47 + .../TestModels/WideArityFixture.cs | 208 ++++ .../TestModels/WideArityObservableFixture.cs | 175 ++++ 12 files changed, 4336 insertions(+), 1 deletion(-) create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs diff --git a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs index d150659..6a3fc3b 100644 --- a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs +++ b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs @@ -15,7 +15,6 @@ namespace ReactiveUI.Binding; /// over these generic stubs due to having fewer optional parameters (or matching type specificity). /// Supports 1-16 property selectors for observation APIs (WhenChanged, WhenChanging, WhenAnyValue). /// -[ExcludeFromCodeCoverage] public static partial class ReactiveUIBindingExtensions { /// diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs new file mode 100644 index 0000000..339e51a --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs @@ -0,0 +1,221 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Binding.Tests.TestModels; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Covers the binding overloads that exist only to be replaced by a generated one. +/// +/// Unlike the observation surface, a binding has no runtime fallback: resolving one needs the two lambdas at +/// compile time. Each overload here therefore refuses the call rather than binding something. They are reached +/// on the declaring class rather than as extension methods, because written as an extension call the generated +/// dispatch wins overload resolution and the refusal never happens. +/// +public class BindingDispatchStubTests +{ + /// The source of a binding. + private readonly DispatchStubViewModel _viewModel = new(); + + /// The target of a binding. + private readonly DispatchStubView _view = new(); + + /// The stream a BindTo call writes from. + private readonly ManualObservable _source = new(); + + /// A one-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindOneWay_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindOneWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// A converting one-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindOneWay_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindOneWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption, + static value => value)) + .ThrowsExactly(); + + /// A two-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTwoWay_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTwoWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// A converting two-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTwoWay_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTwoWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption, + static value => value, + static value => value)) + .ThrowsExactly(); + + /// The view-first spelling of a one-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task OneWayBind_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.OneWayBind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// The converting view-first spelling of a one-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task OneWayBind_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.OneWayBind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption, + static value => value)) + .ThrowsExactly(); + + /// The view-first spelling of a two-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task Bind_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.Bind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// The converting view-first spelling of a two-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task Bind_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.Bind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption, + static value => value, + static value => value)) + .ThrowsExactly(); + + /// Writing a stream into a property with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption)) + .ThrowsExactly(); + + /// Writing a stream into a property with a conversion hint refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithAConversionHint_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption, + conversionHint: null)) + .ThrowsExactly(); + + /// Writing a stream into a property with a converter refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithAConverter_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption, + converterOverride: null)) + .ThrowsExactly(); + + /// Writing a stream into a property with both a hint and a converter refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithAConversionHintAndAConverter_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption, + conversionHint: null, + converterOverride: null)) + .ThrowsExactly(); + + /// A command binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindCommand_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindCommand( + _view, + _viewModel, + x => x.Run, + x => x.Control)) + .ThrowsExactly(); + + /// A command binding taking its parameter from a stream refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindCommand_WithAParameterStream_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindCommand( + _view, + _viewModel, + x => x.Run, + x => x.Control, + _source)) + .ThrowsExactly(); + + /// A command binding taking its parameter from a property refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindCommand_WithAParameterProperty_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindCommand( + _view, + _viewModel, + x => x.Run, + x => x.Control, + x => x.Parameter)) + .ThrowsExactly(); + + /// An interaction binding handled asynchronously refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindInteraction_WithAnAsynchronousHandler_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindInteraction( + _view, + _viewModel, + x => x.Confirm, + static context => Task.CompletedTask)) + .ThrowsExactly(); + + /// An interaction binding handled by a stream refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindInteraction_WithAStreamHandler_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindInteraction( + _view, + _viewModel, + x => x.Confirm, + static IObservable (context) => new ManualObservable())) + .ThrowsExactly(); +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs new file mode 100644 index 0000000..514f29b --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs @@ -0,0 +1,740 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenAnyObservable overloads, which follow the observable a property holds. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenAnyObservableWideArityTests +{ + /// The value each stream is driven with. + private const string EmittedValue = "a"; + + /// The first and last stream's values read together, which is what the selectors project. + private const string BothEnds = EmittedValue + EmittedValue; + + /// Following one stream reports what it emits. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingOneStream_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following two streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingTwoStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following three streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingThreeStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following four streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingFourStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following five streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingFiveStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following six streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingSixStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following seven streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingSevenStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following eight streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingEightStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following nine streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingNineStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following ten streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingTenStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following eleven streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingElevenStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following twelve streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingTwelveStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11, + x => x.Stream12) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Combining two streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningTwoStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining three streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningThreeStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining four streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningFourStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining five streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningFiveStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining six streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningSixStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining seven streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningSevenStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining eight streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningEightStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining nine streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningNineStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining ten streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningTenStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining eleven streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningElevenStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining twelve streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningTwelveStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11, + x => x.Stream12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs new file mode 100644 index 0000000..33b6119 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs @@ -0,0 +1,854 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenAnyValue overloads, the ReactiveUI-compatible spelling of WhenChanged. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenAnyValueWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnOneProperty_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Observing two properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnTwoProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2) + .Subscribe(v => seen.Add(v.Property1 + v.Property2)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing three properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnThreeProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3) + .Subscribe(v => seen.Add(v.Property1 + v.Property3)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing four properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFourProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4) + .Subscribe(v => seen.Add(v.Property1 + v.Property4)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing five properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFiveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5) + .Subscribe(v => seen.Add(v.Property1 + v.Property5)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing six properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnSixProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6) + .Subscribe(v => seen.Add(v.Property1 + v.Property6)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing seven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnSevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7) + .Subscribe(v => seen.Add(v.Property1 + v.Property7)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eight properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnEightProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8) + .Subscribe(v => seen.Add(v.Property1 + v.Property8)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing nine properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnNineProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9) + .Subscribe(v => seen.Add(v.Property1 + v.Property9)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing ten properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnTenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10) + .Subscribe(v => seen.Add(v.Property1 + v.Property10)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnElevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11) + .Subscribe(v => seen.Add(v.Property1 + v.Property11)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnTwelveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12) + .Subscribe(v => seen.Add(v.Property1 + v.Property12)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing thirteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnThirteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13) + .Subscribe(v => seen.Add(v.Property1 + v.Property13)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fourteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFourteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14) + .Subscribe(v => seen.Add(v.Property1 + v.Property14)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fifteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFifteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15) + .Subscribe(v => seen.Add(v.Property1 + v.Property15)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing sixteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnSixteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16) + .Subscribe(v => seen.Add(v.Property1 + v.Property16)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting one observed property reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingOneProperty_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + static (string v1) => v1) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Projecting two observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting three observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting four observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting five observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting six observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting seven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eight observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting nine observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting ten observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eleven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting twelve observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting thirteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingThirteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) => v1 + v13) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fourteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFourteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) => v1 + v14) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fifteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFifteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) => v1 + v15) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting sixteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingSixteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) => v1 + v16) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs new file mode 100644 index 0000000..bb4ec92 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs @@ -0,0 +1,316 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenAny overloads, which hand the selector an observed change rather than a bare value. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenAnyWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnOneProperty_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + static c1 => c1.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Observing two properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + static (c1, c2) => c1.Value + c2.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing three properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (c1, c2, c3) => c1.Value + c3.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing four properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (c1, c2, c3, c4) => c1.Value + c4.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing five properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (c1, c2, c3, c4, c5) => c1.Value + c5.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing six properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (c1, c2, c3, c4, c5, c6) => c1.Value + c6.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing seven properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (c1, c2, c3, c4, c5, c6, c7) => c1.Value + c7.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eight properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (c1, c2, c3, c4, c5, c6, c7, c8) => c1.Value + c8.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing nine properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9) => c1.Value + c9.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing ten properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => c1.Value + c10.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => c1.Value + c11.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => c1.Value + c12.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs new file mode 100644 index 0000000..b6abd20 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs @@ -0,0 +1,835 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenChanged overloads, which observe after a value changes. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenChangedWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnOneProperty_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Observing two properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnTwoProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2) + .Subscribe(v => seen.Add(v.Property1 + v.Property2)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing three properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnThreeProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3) + .Subscribe(v => seen.Add(v.Property1 + v.Property3)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing four properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFourProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4) + .Subscribe(v => seen.Add(v.Property1 + v.Property4)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing five properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFiveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5) + .Subscribe(v => seen.Add(v.Property1 + v.Property5)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing six properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnSixProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6) + .Subscribe(v => seen.Add(v.Property1 + v.Property6)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing seven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnSevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7) + .Subscribe(v => seen.Add(v.Property1 + v.Property7)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eight properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnEightProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8) + .Subscribe(v => seen.Add(v.Property1 + v.Property8)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing nine properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnNineProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9) + .Subscribe(v => seen.Add(v.Property1 + v.Property9)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing ten properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnTenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10) + .Subscribe(v => seen.Add(v.Property1 + v.Property10)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnElevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11) + .Subscribe(v => seen.Add(v.Property1 + v.Property11)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnTwelveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12) + .Subscribe(v => seen.Add(v.Property1 + v.Property12)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing thirteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnThirteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13) + .Subscribe(v => seen.Add(v.Property1 + v.Property13)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fourteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFourteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14) + .Subscribe(v => seen.Add(v.Property1 + v.Property14)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fifteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFifteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15) + .Subscribe(v => seen.Add(v.Property1 + v.Property15)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing sixteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnSixteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16) + .Subscribe(v => seen.Add(v.Property1 + v.Property16)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting two observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting three observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting four observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting five observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting six observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting seven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eight observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting nine observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting ten observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eleven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting twelve observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting thirteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingThirteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) => v1 + v13) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fourteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFourteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) => v1 + v14) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fifteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFifteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) => v1 + v15) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting sixteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingSixteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) => v1 + v16) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs new file mode 100644 index 0000000..78eaed7 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs @@ -0,0 +1,897 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenChanging overloads, which observe before a value changes. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenChangingWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnOneProperty_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(InitialValue); + } + + /// Observing two properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnTwoProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2) + .Subscribe(v => seen.Add(v.Property1 + v.Property2)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing three properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnThreeProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3) + .Subscribe(v => seen.Add(v.Property1 + v.Property3)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing four properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFourProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4) + .Subscribe(v => seen.Add(v.Property1 + v.Property4)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing five properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFiveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5) + .Subscribe(v => seen.Add(v.Property1 + v.Property5)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing six properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnSixProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6) + .Subscribe(v => seen.Add(v.Property1 + v.Property6)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing seven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnSevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7) + .Subscribe(v => seen.Add(v.Property1 + v.Property7)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing eight properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnEightProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8) + .Subscribe(v => seen.Add(v.Property1 + v.Property8)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing nine properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnNineProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9) + .Subscribe(v => seen.Add(v.Property1 + v.Property9)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing ten properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnTenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10) + .Subscribe(v => seen.Add(v.Property1 + v.Property10)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnElevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11) + .Subscribe(v => seen.Add(v.Property1 + v.Property11)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnTwelveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12) + .Subscribe(v => seen.Add(v.Property1 + v.Property12)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing thirteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnThirteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13) + .Subscribe(v => seen.Add(v.Property1 + v.Property13)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing fourteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFourteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14) + .Subscribe(v => seen.Add(v.Property1 + v.Property14)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing fifteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFifteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15) + .Subscribe(v => seen.Add(v.Property1 + v.Property15)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing sixteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnSixteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16) + .Subscribe(v => seen.Add(v.Property1 + v.Property16)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting two observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting three observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting four observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting five observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting six observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting seven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting eight observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting nine observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting ten observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting eleven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting twelve observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting thirteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingThirteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) => v1 + v13) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting fourteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFourteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) => v1 + v14) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting fifteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFifteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) => v1 + v15) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting sixteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingSixteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) => v1 + v16) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs new file mode 100644 index 0000000..6dd7822 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// A control a command binding names, carrying nothing beyond a property to bind against. +public class DispatchStubControl +{ + /// Gets or sets the control's text. + public string Text { get; set; } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs new file mode 100644 index 0000000..df489d1 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// A view naming the properties the binding dispatch stubs bind against. +public class DispatchStubView : IViewFor, INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public object? ViewModel { get; set; } + + /// Gets the control a command binding names. + public DispatchStubControl Control { get; } = new(); + + /// Gets or sets the bound text. + public string Caption + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Caption))); + } + } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs new file mode 100644 index 0000000..f588b87 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; +using System.Windows.Input; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// A view model naming one property of each kind the binding dispatch stubs take. +/// +/// The command and interaction are never invoked: a stub throws before it reads either, so the +/// properties exist to satisfy the overloads' type constraints rather than to behave. +/// +public class DispatchStubViewModel : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets the bound text. + public string Caption + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Caption))); + } + } = "a"; + + /// Gets or sets the value a command binding passes as its parameter. + public string Parameter + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Parameter))); + } + } = "a"; + + /// Gets or sets the command a command binding names. + public ICommand? Run { get; set; } + + /// Gets or sets the interaction an interaction binding names. + public IInteraction Confirm { get; set; } = null!; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs new file mode 100644 index 0000000..d6b1a77 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs @@ -0,0 +1,208 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object with enough notifying properties to reach every wide-arity observation overload. +public class WideArityFixture : INotifyPropertyChanged, INotifyPropertyChanging +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// Gets or sets observed property 1. + public string Value1 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value1))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value1))); + } + } = "a"; + + /// Gets or sets observed property 2. + public string Value2 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value2))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value2))); + } + } = "a"; + + /// Gets or sets observed property 3. + public string Value3 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value3))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value3))); + } + } = "a"; + + /// Gets or sets observed property 4. + public string Value4 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value4))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value4))); + } + } = "a"; + + /// Gets or sets observed property 5. + public string Value5 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value5))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value5))); + } + } = "a"; + + /// Gets or sets observed property 6. + public string Value6 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value6))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value6))); + } + } = "a"; + + /// Gets or sets observed property 7. + public string Value7 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value7))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value7))); + } + } = "a"; + + /// Gets or sets observed property 8. + public string Value8 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value8))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value8))); + } + } = "a"; + + /// Gets or sets observed property 9. + public string Value9 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value9))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value9))); + } + } = "a"; + + /// Gets or sets observed property 10. + public string Value10 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value10))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value10))); + } + } = "a"; + + /// Gets or sets observed property 11. + public string Value11 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value11))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value11))); + } + } = "a"; + + /// Gets or sets observed property 12. + public string Value12 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value12))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value12))); + } + } = "a"; + + /// Gets or sets observed property 13. + public string Value13 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value13))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value13))); + } + } = "a"; + + /// Gets or sets observed property 14. + public string Value14 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value14))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value14))); + } + } = "a"; + + /// Gets or sets observed property 15. + public string Value15 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value15))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value15))); + } + } = "a"; + + /// Gets or sets observed property 16. + public string Value16 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value16))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value16))); + } + } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs new file mode 100644 index 0000000..741f63f --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object with enough observable properties to reach every wide-arity WhenAnyObservable overload. +public class WideArityObservableFixture : INotifyPropertyChanged +{ + /// How many streams the fixture carries, which is the widest arity these overloads reach. + private const int StreamCount = 12; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets observed stream 1. + public IObservable? Stream1 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream1))); + } + } + + /// Gets or sets observed stream 2. + public IObservable? Stream2 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream2))); + } + } + + /// Gets or sets observed stream 3. + public IObservable? Stream3 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream3))); + } + } + + /// Gets or sets observed stream 4. + public IObservable? Stream4 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream4))); + } + } + + /// Gets or sets observed stream 5. + public IObservable? Stream5 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream5))); + } + } + + /// Gets or sets observed stream 6. + public IObservable? Stream6 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream6))); + } + } + + /// Gets or sets observed stream 7. + public IObservable? Stream7 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream7))); + } + } + + /// Gets or sets observed stream 8. + public IObservable? Stream8 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream8))); + } + } + + /// Gets or sets observed stream 9. + public IObservable? Stream9 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream9))); + } + } + + /// Gets or sets observed stream 10. + public IObservable? Stream10 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream10))); + } + } + + /// Gets or sets observed stream 11. + public IObservable? Stream11 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream11))); + } + } + + /// Gets or sets observed stream 12. + public IObservable? Stream12 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream12))); + } + } + + /// Fills every stream with a manually driven observable and hands them back in order. + /// The observables the streams were filled with. + internal ManualObservable[] FillStreams() + { + var streams = new ManualObservable[StreamCount]; + for (var i = 0; i < streams.Length; i++) + { + streams[i] = new(); + } + + Stream1 = streams[0]; + Stream2 = streams[1]; + Stream3 = streams[2]; + Stream4 = streams[3]; + Stream5 = streams[4]; + Stream6 = streams[5]; + Stream7 = streams[6]; + Stream8 = streams[7]; + Stream9 = streams[8]; + Stream10 = streams[9]; + Stream11 = streams[10]; + Stream12 = streams[11]; + + return streams; + } +} From 360a0297b15bf90f40803a27491ae4475f52db5f Mon Sep 17 00:00:00 2001 From: Glenn Watson <5834289+glennawatson@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:15:58 +1000 Subject: [PATCH 3/4] fix(benchmarks): annotate the dynamic-chain benchmarks at class scope - RequiresUnreferencedCode is a .NET 5+ type. On net462 the only one in reach is the runtime library's internal polyfill, which another assembly cannot apply, and no trim analyzer runs there to need the annotation. - One annotation per class covers every benchmark it holds, so the framework guard is a single conditional in each file. --- .../RxUiDynamicChainBaseline.cs | 9 +++++---- .../WhenAnyDynamicBenchmark.cs | 11 ++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs index ba5739e..a68901d 100644 --- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs +++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs @@ -2,7 +2,9 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +#if NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Linq.Expressions; using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; @@ -30,6 +32,9 @@ namespace RxUiDynamicChain; [MemoryDiagnoser] [EventPipeProfiler(EventPipeProfile.GcVerbose)] [MarkdownExporterAttribute.GitHub] +#if NET8_0_OR_GREATER +[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] +#endif public class RxUiDynamicChainBaseline { /// Represents the number of property change events to be triggered during the benchmark tests. @@ -75,7 +80,6 @@ public void Setup() => _vm = new() { Name = "Initial", Age = 0, Child = new() { Value = "ChildInitial" } }; /// One chain: subscribe, fire N changes, dispose. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "Single Chain")] public void SingleChain() { @@ -90,7 +94,6 @@ public void SingleChain() } /// Two chains combined: subscribe, fire N changes on each, dispose. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "Two Chains")] public void TwoChains() { @@ -106,7 +109,6 @@ public void TwoChains() } /// A chain through an intermediate: subscribe, fire N changes on the leaf, dispose. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "Deep Chain")] public void DeepChain() { @@ -122,7 +124,6 @@ public void DeepChain() /// Cold start: subscribe, read the initial value, dispose. No property changes. /// The observed value. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "First Observation")] public object? FirstObservation() { diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs index 2f547b4..7a828f5 100644 --- a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs +++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs @@ -2,7 +2,9 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +#if NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Linq.Expressions; using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; @@ -19,7 +21,7 @@ namespace ReactiveUI.Binding.Benchmarks; /// /// /// No NativeAOT job: these overloads walk members by reflection and say so with -/// , so an ahead-of-time published run is not a +/// RequiresUnreferencedCode, so an ahead-of-time published run is not a /// configuration they support. /// [SimpleJob(RuntimeMoniker.Net80)] @@ -27,6 +29,9 @@ namespace ReactiveUI.Binding.Benchmarks; [MemoryDiagnoser] [EventPipeProfiler(EventPipeProfile.GcVerbose)] [MarkdownExporterAttribute.GitHub] +#if NET8_0_OR_GREATER +[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] +#endif public class WhenAnyDynamicBenchmark { /// Represents the number of property change events to be triggered during the benchmark tests. @@ -59,7 +64,6 @@ public void Setup() => _vm = new() { Name = "Initial", Age = 0, Child = new() { Value = "ChildInitial" } }; /// One chain: subscribe, fire N changes, dispose. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "Single Chain")] public void SingleChain() { @@ -74,7 +78,6 @@ public void SingleChain() } /// Two chains combined: subscribe, fire N changes on each, dispose. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "Two Chains")] public void TwoChains() { @@ -90,7 +93,6 @@ public void TwoChains() } /// A chain through an intermediate: subscribe, fire N changes on the leaf, dispose. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "Deep Chain")] public void DeepChain() { @@ -106,7 +108,6 @@ public void DeepChain() /// Cold start: subscribe, read the initial value, dispose. No property changes. /// The observed value. - [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] [Benchmark(Description = "First Observation")] public object? FirstObservation() { From 446c1206ea3721c272cd8fdc4258a5d9b875325d Mon Sep 17 00:00:00 2001 From: Glenn Watson <5834289+glennawatson@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:32:03 +1000 Subject: [PATCH 4/4] refactor(observation): funnel the dynamic-chain overloads through one helper - Each overload is its signature and two lines: argument checks and the subscriptions move into a shared helper, so the reflection and the refusals are written once rather than once per arity. - Name the file for the arity-expanded surface it is, alongside the other *.WideArity.cs mixins. The repetition left is the twelve public signatures, which is what an arity-expanded API is. --- ...ingExtensions.WhenAnyDynamic.WideArity.cs} | 186 +++++------------- 1 file changed, 53 insertions(+), 133 deletions(-) rename src/ReactiveUI.Binding.Shared/Mixins/{ReactiveUIBindingExtensions.WhenAnyDynamic.cs => ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs} (87%) diff --git a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs similarity index 87% rename from src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs rename to src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs index 44bc316..727fd8b 100644 --- a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.cs +++ b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs @@ -61,10 +61,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return ObserveDynamicChain(sender, property1, isDistinct).Select(selector); + var chains = Chains(sender, selector, isDistinct, property1); + return chains[0].Select(selector); } /// Observes 2 dynamically-typed property chains and combines them with a selector. @@ -103,13 +101,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2); + return CombineLatestObservable.Create(chains[0], chains[1], selector); } /// Observes 3 dynamically-typed property chains and combines them with a selector. @@ -152,14 +145,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], selector); } /// Observes 4 dynamically-typed property chains and combines them with a selector. @@ -216,15 +203,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], selector); } /// Observes 5 dynamically-typed property chains and combines them with a selector. @@ -287,16 +267,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], selector); } /// Observes 6 dynamically-typed property chains and combines them with a selector. @@ -366,17 +338,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], selector); } /// Observes 7 dynamically-typed property chains and combines them with a selector. @@ -453,18 +416,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - ObserveDynamicChain(sender, property7, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], selector); } /// Observes 8 dynamically-typed property chains and combines them with a selector. @@ -547,19 +500,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - ObserveDynamicChain(sender, property7, isDistinct), - ObserveDynamicChain(sender, property8, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], selector); } /// Observes 9 dynamically-typed property chains and combines them with a selector. @@ -648,20 +590,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - ObserveDynamicChain(sender, property7, isDistinct), - ObserveDynamicChain(sender, property8, isDistinct), - ObserveDynamicChain(sender, property9, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], selector); } /// Observes 10 dynamically-typed property chains and combines them with a selector. @@ -756,21 +686,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - ObserveDynamicChain(sender, property7, isDistinct), - ObserveDynamicChain(sender, property8, isDistinct), - ObserveDynamicChain(sender, property9, isDistinct), - ObserveDynamicChain(sender, property10, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9, property10); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], chains[9], selector); } /// Observes 11 dynamically-typed property chains and combines them with a selector. @@ -871,22 +788,8 @@ public static IObservable WhenAnyDynamic( bool isDistinct) where TSender : class { - ArgumentExceptionHelper.ThrowIfNull(sender); - ArgumentExceptionHelper.ThrowIfNull(selector); - - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - ObserveDynamicChain(sender, property7, isDistinct), - ObserveDynamicChain(sender, property8, isDistinct), - ObserveDynamicChain(sender, property9, isDistinct), - ObserveDynamicChain(sender, property10, isDistinct), - ObserveDynamicChain(sender, property11, isDistinct), - selector); + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], chains[9], chains[10], selector); } /// Observes 12 dynamically-typed property chains and combines them with a selector. @@ -992,24 +895,41 @@ public static IObservable WhenAnyDynamic( TRet> selector, bool isDistinct) where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], chains[9], chains[10], chains[11], selector); + } + + /// Subscribes to every chain an overload was handed, after refusing a missing argument. + /// The type of the object the chains are rooted on. + /// The object the chains are rooted on. + /// The projection the caller supplied, which is required. + /// Whether a chain reports only when its value changes. + /// The expressions naming the chains. + /// One observation per chain, in the order the chains were given. + /// + /// Every arity funnels its argument checks and its subscriptions through here, so each overload is the + /// signature and nothing else. The array is built once when the observable is created rather than per + /// notification, so a higher arity costs one allocation at subscription and nothing while it reports. + /// + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + private static IObservable>[] Chains( + TSender sender, + object selector, + bool isDistinct, + params Expression?[] properties) + where TSender : class { ArgumentExceptionHelper.ThrowIfNull(sender); ArgumentExceptionHelper.ThrowIfNull(selector); - return CombineLatestObservable.Create( - ObserveDynamicChain(sender, property1, isDistinct), - ObserveDynamicChain(sender, property2, isDistinct), - ObserveDynamicChain(sender, property3, isDistinct), - ObserveDynamicChain(sender, property4, isDistinct), - ObserveDynamicChain(sender, property5, isDistinct), - ObserveDynamicChain(sender, property6, isDistinct), - ObserveDynamicChain(sender, property7, isDistinct), - ObserveDynamicChain(sender, property8, isDistinct), - ObserveDynamicChain(sender, property9, isDistinct), - ObserveDynamicChain(sender, property10, isDistinct), - ObserveDynamicChain(sender, property11, isDistinct), - ObserveDynamicChain(sender, property12, isDistinct), - selector); + var chains = new IObservable>[properties.Length]; + for (var i = 0; i < properties.Length; i++) + { + chains[i] = ObserveDynamicChain(sender, properties[i], isDistinct); + } + + return chains; } /// Subscribes to one property chain named by a run-time expression.