From 9df16c40ad2f99d881dcced353b3a48f13ca8d77 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sun, 20 Sep 2026 22:06:08 -0700 Subject: [PATCH] Validate legacy TransformSafeAsync factories before forwarding --- ...formSafeAsyncFixture.ArgumentValidation.cs | 52 +++++++++++++++++++ .../Cache/TransformSafeAsyncFixture.cs | 4 +- .../ObservableCacheEx.TransformSafeAsync.cs | 4 ++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.ArgumentValidation.cs diff --git a/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.ArgumentValidation.cs b/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.ArgumentValidation.cs new file mode 100644 index 000000000..99cdd2455 --- /dev/null +++ b/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.ArgumentValidation.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2011-2026 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Threading.Tasks; + +using Randomizer = Bogus.Randomizer; + +using DynamicData.Kernel; +using DynamicData.Tests.Domain; + +using FluentAssertions; + +using Xunit; +using Xunit.Abstractions; + +namespace DynamicData.Tests.Cache; + +public partial class TransformSafeAsyncFixture +{ + private const int ArgumentValidationSeed = 0x2409_1165; + + private readonly Randomizer _argumentRandomizer = new(ArgumentValidationSeed); + + public TransformSafeAsyncFixture(ITestOutputHelper output) + => output.WriteLine($"{nameof(TransformSafeAsyncFixture)} seed: {ArgumentValidationSeed:X8}"); + + /// Verifies that the options overload rejects a null factory without subscribing to the source. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void OptionsOverload_NullFactory_ThrowsBeforeSubscription(bool populateSource) + { + // Arrange + using var source = new SourceCache(static person => person.Key); + + if (populateSource) + { + source.AddOrUpdate(new Person(_argumentRandomizer.String2(_argumentRandomizer.Int(5, 20)), _argumentRandomizer.Int(1, 100))); + } + + Func, string, Task>? transformFactory = null; + + // Act + Action action = () => source.Connect().TransformSafeAsync(transformFactory!, static _ => { }, TransformAsyncOptions.Default); + + // Assert + action.Should().Throw(because: "invalid factories must be rejected before processing any items") + .WithParameterName(nameof(transformFactory)); + } +} diff --git a/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs b/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs index b0b26cfb6..8509a016c 100644 --- a/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reactive; @@ -13,7 +13,7 @@ namespace DynamicData.Tests.Cache; -public class TransformSafeAsyncFixture +public partial class TransformSafeAsyncFixture { [Fact] public void ReTransformAll() diff --git a/src/DynamicData/Cache/ObservableCacheEx.TransformSafeAsync.cs b/src/DynamicData/Cache/ObservableCacheEx.TransformSafeAsync.cs index 70477e563..a33d6cdea 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.TransformSafeAsync.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.TransformSafeAsync.cs @@ -134,6 +134,10 @@ public static IObservable> TransformSafeAsync transformFactory(current, previous, key), errorHandler, options); }